Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
979508047c | ||
![]() |
bdaadbecfc | ||
![]() |
f037bf3a35 | ||
![]() |
0b52c5b05f | ||
![]() |
1035e911bb | ||
![]() |
f8955ec25a | ||
![]() |
670da1ec26 | ||
![]() |
42890cb79f | ||
![]() |
046ef51293 | ||
![]() |
1b3866568f | ||
![]() |
51e23b7eca |
832 changed files with 101362 additions and 37087 deletions
|
@ -4,18 +4,10 @@ on:
|
|||
push:
|
||||
branches: '**'
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
branches: [ main, develop, canary ]
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
check_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Body
|
||||
uses: JJ/github-pr-contains-action@releases/v10
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
bodyDoesNotContain: "[\"|`]"
|
||||
build:
|
||||
name: Build .Net
|
||||
runs-on: windows-latest
|
||||
|
@ -37,18 +29,19 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 1.11
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: csproj
|
||||
path: Kavita.Common/Kavita.Common.csproj
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~\sonar\cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
|
@ -56,7 +49,7 @@ jobs:
|
|||
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
|
@ -83,10 +76,10 @@ jobs:
|
|||
run: dotnet test --no-restore --verbosity normal
|
||||
|
||||
version:
|
||||
name: Bump version on Develop push
|
||||
name: Bump version on Develop/Canary push
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/canary') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -97,15 +90,6 @@ jobs:
|
|||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Install Swashbuckle CLI
|
||||
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Bump versions
|
||||
uses: SiqiLu/dotnet-bump-version@2.0.0
|
||||
with:
|
||||
|
@ -123,9 +107,10 @@ jobs:
|
|||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
steps:
|
||||
- name: Find Current Pull Request
|
||||
uses: jwalton/gh-find-current-pr@v1.0.2
|
||||
uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
with:
|
||||
state: all
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Parse PR body
|
||||
|
@ -146,7 +131,7 @@ jobs:
|
|||
body=${body//$'`'/'%60'}
|
||||
body=${body//$'>'/'%3E'}
|
||||
echo $body
|
||||
echo "::set-output name=BODY::$body"
|
||||
echo "BODY=$body" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v3
|
||||
|
@ -154,13 +139,13 @@ jobs:
|
|||
ref: develop
|
||||
|
||||
- name: NodeJS to Compile WebUI
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- run: |
|
||||
cd UI/Web || exit
|
||||
echo 'Installing web dependencies'
|
||||
npm ci
|
||||
npm install --legacy-peer-deps
|
||||
|
||||
echo 'Building UI'
|
||||
npm run prod
|
||||
|
@ -171,7 +156,7 @@ jobs:
|
|||
cd ../ || exit
|
||||
|
||||
- name: Get csproj Version
|
||||
uses: naminodarie/get-net-sdk-project-versions-action@v1
|
||||
uses: kzrnm/get-net-sdk-project-versions-action@v1
|
||||
id: get-version
|
||||
with:
|
||||
proj-path: Kavita.Common/Kavita.Common.csproj
|
||||
|
@ -179,7 +164,7 @@ jobs:
|
|||
- name: Parse Version
|
||||
run: |
|
||||
version='${{steps.get-version.outputs.assembly-version}}'
|
||||
echo "::set-output name=VERSION::$version"
|
||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||
id: parse-version
|
||||
|
||||
- name: Echo csproj version
|
||||
|
@ -209,15 +194,15 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
|
@ -232,7 +217,7 @@ jobs:
|
|||
with:
|
||||
severity: info
|
||||
description: v${{steps.get-version.outputs.assembly-version}} - ${{ steps.findPr.outputs.title }}
|
||||
details: '${{ steps.parse-body.outputs.BODY }}'
|
||||
details: '${{ steps.findPr.outputs.body }}'
|
||||
text: <@&939225459156217917> <@&939225350775406643> A new nightly build has been released for docker.
|
||||
webhookUrl: ${{ secrets.DISCORD_DOCKER_UPDATE_URL }}
|
||||
|
||||
|
@ -243,13 +228,14 @@ jobs:
|
|||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }}
|
||||
steps:
|
||||
|
||||
- name: Find Current Pull Request
|
||||
uses: jwalton/gh-find-current-pr@v1.0.2
|
||||
uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
with:
|
||||
state: all
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Parse PR body
|
||||
|
@ -270,7 +256,8 @@ jobs:
|
|||
body=${body//$'`'/'%60'}
|
||||
body=${body//$'>'/'%3E'}
|
||||
echo $body
|
||||
echo "::set-output name=BODY::$body"
|
||||
echo "BODY=$body" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v3
|
||||
|
@ -278,14 +265,14 @@ jobs:
|
|||
ref: main
|
||||
|
||||
- name: NodeJS to Compile WebUI
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- run: |
|
||||
|
||||
cd UI/Web || exit
|
||||
echo 'Installing web dependencies'
|
||||
npm install
|
||||
npm install --legacy-peer-deps
|
||||
|
||||
echo 'Building UI'
|
||||
npm run prod
|
||||
|
@ -296,7 +283,7 @@ jobs:
|
|||
cd ../ || exit
|
||||
|
||||
- name: Get csproj Version
|
||||
uses: naminodarie/get-net-sdk-project-versions-action@v1
|
||||
uses: kzrnm/get-net-sdk-project-versions-action@v1
|
||||
id: get-version
|
||||
with:
|
||||
proj-path: Kavita.Common/Kavita.Common.csproj
|
||||
|
@ -309,7 +296,7 @@ jobs:
|
|||
version='${{steps.get-version.outputs.assembly-version}}'
|
||||
newVersion=${version%.*}
|
||||
echo $newVersion
|
||||
echo "::set-output name=VERSION::$newVersion"
|
||||
echo "VERSION=$newVersion" >> $GITHUB_OUTPUT
|
||||
id: parse-version
|
||||
|
||||
- name: Compile dotnet app
|
||||
|
@ -335,15 +322,15 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
|
@ -358,6 +345,101 @@ jobs:
|
|||
with:
|
||||
severity: info
|
||||
description: v${{steps.get-version.outputs.assembly-version}} - ${{ steps.findPr.outputs.title }}
|
||||
details: '${{ steps.parse-body.outputs.BODY }}'
|
||||
details: '${{ steps.findPr.outputs.body }}'
|
||||
text: <@&939225192553644133> A new stable build has been released.
|
||||
webhookUrl: ${{ secrets.DISCORD_DOCKER_UPDATE_URL }}
|
||||
|
||||
canary:
|
||||
name: Build Canary Docker if Canary push
|
||||
needs: [ build, version ]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/canary' }}
|
||||
steps:
|
||||
- name: Find Current Pull Request
|
||||
uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
with:
|
||||
state: all
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: canary
|
||||
|
||||
- name: NodeJS to Compile WebUI
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: |
|
||||
cd UI/Web || exit
|
||||
echo 'Installing web dependencies'
|
||||
npm install --legacy-peer-deps
|
||||
|
||||
echo 'Building UI'
|
||||
npm run prod
|
||||
|
||||
echo 'Copying back to Kavita wwwroot'
|
||||
rsync -a dist/ ../../API/wwwroot/
|
||||
|
||||
cd ../ || exit
|
||||
|
||||
- name: Get csproj Version
|
||||
uses: kzrnm/get-net-sdk-project-versions-action@v1
|
||||
id: get-version
|
||||
with:
|
||||
proj-path: Kavita.Common/Kavita.Common.csproj
|
||||
|
||||
- name: Parse Version
|
||||
run: |
|
||||
version='${{steps.get-version.outputs.assembly-version}}'
|
||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||
id: parse-version
|
||||
|
||||
- name: Echo csproj version
|
||||
run: echo "${{steps.get-version.outputs.assembly-version}}"
|
||||
|
||||
- name: Compile dotnet app
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Install Swashbuckle CLI
|
||||
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
- run: ./monorepo-build.sh
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: kizaing/kavita:canary, kizaing/kavita:canary-${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:canary, ghcr.io/kareadita/kavita:canary-${{ steps.parse-version.outputs.VERSION }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
18
.github/workflows/pr-check.yml
vendored
Normal file
18
.github/workflows/pr-check.yml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
name: Validate PR Body
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: '**'
|
||||
pull_request:
|
||||
branches: [ main, develop, canary ]
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
check_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Body
|
||||
uses: JJ/github-pr-contains-action@releases/v10
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
bodyDoesNotContain: "[\"|`]"
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -512,6 +512,7 @@ UI/Web/dist/
|
|||
/API/config/themes/
|
||||
/API/config/stats/
|
||||
/API/config/bookmarks/
|
||||
/API/config/favicons/
|
||||
/API/config/kavita.db
|
||||
/API/config/kavita.db-shm
|
||||
/API/config/kavita.db-wal
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.5" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.7" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.7" />
|
||||
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||
using API.Services;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Order;
|
||||
using EasyCaching.Core;
|
||||
using NSubstitute;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.Formats.Webp;
|
||||
|
@ -30,8 +32,8 @@ public class ArchiveServiceBenchmark
|
|||
public ArchiveServiceBenchmark()
|
||||
{
|
||||
_directoryService = new DirectoryService(null, new FileSystem());
|
||||
_imageService = new ImageService(null, _directoryService);
|
||||
_archiveService = new ArchiveService(new NullLogger<ArchiveService>(), _directoryService, _imageService);
|
||||
_imageService = new ImageService(null, _directoryService, Substitute.For<IEasyCachingProviderFactory>());
|
||||
_archiveService = new ArchiveService(new NullLogger<ArchiveService>(), _directoryService, _imageService, Substitute.For<IMediaErrorService>());
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.Services;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Order;
|
||||
using HtmlAgilityPack;
|
||||
using VersOne.Epub;
|
||||
|
||||
namespace API.Benchmark;
|
||||
|
||||
[StopOnFirstError]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||
[SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)]
|
||||
public class EpubBenchmark
|
||||
{
|
||||
private const string FilePath = @"E:\Books\Invaders of the Rokujouma\Invaders of the Rokujouma - Volume 01.epub";
|
||||
private readonly Regex _wordRegex = new Regex(@"\b\w+\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
[Benchmark]
|
||||
public async Task GetWordCount_PassByRef()
|
||||
{
|
||||
using var book = await EpubReader.OpenBookAsync(FilePath, BookService.BookReaderOptions);
|
||||
foreach (var bookFile in book.Content.Html.Values)
|
||||
{
|
||||
await GetBookWordCount_PassByRef(bookFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public async Task GetBookWordCount_SumEarlier()
|
||||
{
|
||||
using var book = await EpubReader.OpenBookAsync(FilePath, BookService.BookReaderOptions);
|
||||
foreach (var bookFile in book.Content.Html.Values)
|
||||
{
|
||||
await GetBookWordCount_SumEarlier(bookFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public async Task GetBookWordCount_Regex()
|
||||
{
|
||||
using var book = await EpubReader.OpenBookAsync(FilePath, BookService.BookReaderOptions);
|
||||
foreach (var bookFile in book.Content.Html.Values)
|
||||
{
|
||||
await GetBookWordCount_Regex(bookFile);
|
||||
}
|
||||
}
|
||||
|
||||
private int GetBookWordCount_PassByString(string fileContents)
|
||||
{
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(fileContents);
|
||||
var delimiter = new char[] {' '};
|
||||
|
||||
return doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]")
|
||||
.Select(node => node.InnerText)
|
||||
.Select(text => text.Split(delimiter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(s => char.IsLetter(s[0])))
|
||||
.Select(words => words.Count())
|
||||
.Where(wordCount => wordCount > 0)
|
||||
.Sum();
|
||||
}
|
||||
|
||||
private async Task<int> GetBookWordCount_PassByRef(EpubContentFileRef bookFile)
|
||||
{
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(await bookFile.ReadContentAsTextAsync());
|
||||
var delimiter = new char[] {' '};
|
||||
|
||||
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
|
||||
if (textNodes == null) return 0;
|
||||
return textNodes.Select(node => node.InnerText)
|
||||
.Select(text => text.Split(delimiter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(s => char.IsLetter(s[0])))
|
||||
.Select(words => words.Count())
|
||||
.Where(wordCount => wordCount > 0)
|
||||
.Sum();
|
||||
}
|
||||
|
||||
private async Task<int> GetBookWordCount_SumEarlier(EpubContentFileRef bookFile)
|
||||
{
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(await bookFile.ReadContentAsTextAsync());
|
||||
|
||||
return doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]")
|
||||
.DefaultIfEmpty()
|
||||
.Select(node => node.InnerText.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(s => char.IsLetter(s[0])))
|
||||
.Sum(words => words.Count());
|
||||
}
|
||||
|
||||
private async Task<int> GetBookWordCount_Regex(EpubContentFileRef bookFile)
|
||||
{
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(await bookFile.ReadContentAsTextAsync());
|
||||
|
||||
|
||||
return doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]")
|
||||
.Sum(node => _wordRegex.Matches(node.InnerText).Count);
|
||||
}
|
||||
}
|
|
@ -6,18 +6,17 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NSubstitute" Version="4.4.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.11" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.11" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.51" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.51" />
|
||||
<PackageReference Include="xunit" Version="2.5.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace API.Tests.Extensions;
|
|||
public class SeriesExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetCoverImage_MultipleSpecials_Comics()
|
||||
public void GetCoverImage_MultipleSpecials()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
|
@ -29,33 +29,93 @@ public class SeriesExtensionsTests
|
|||
.Build())
|
||||
.Build();
|
||||
|
||||
Assert.Equal("Special 1", series.GetCoverImage());
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Special 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_MultipleSpecials_Books()
|
||||
public void GetCoverImage_Volume1Chapter1_Volume2_AndLooseChapters()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithIsSpecial(true)
|
||||
.WithChapter(new ChapterBuilder("13")
|
||||
.WithCoverImage("Chapter 13")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithCoverImage("Special 2")
|
||||
.WithIsSpecial(true)
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithName("Volume 1")
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.WithCoverImage("Volume 1 Chapter 1")
|
||||
.Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithName("Volume 2")
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithCoverImage("Volume 2")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
Assert.Equal("Special 1", series.GetCoverImage());
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_JustChapters_Comics()
|
||||
public void GetCoverImage_JustVolumes()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithName("Volume 1")
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithCoverImage("Volume 1 Chapter 1")
|
||||
.Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithName("Volume 2")
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithCoverImage("Volume 2")
|
||||
.Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithName("Volume 3")
|
||||
.WithChapter(new ChapterBuilder("10")
|
||||
.WithCoverImage("Volume 3 Chapter 10")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("11")
|
||||
.WithCoverImage("Volume 3 Chapter 11")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("12")
|
||||
.WithCoverImage("Volume 3 Chapter 12")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_JustSpecials_WithDecimal()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
|
@ -81,7 +141,7 @@ public class SeriesExtensionsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_JustChaptersAndSpecials_Comics()
|
||||
public void GetCoverImage_JustChaptersAndSpecials()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
|
@ -89,15 +149,15 @@ public class SeriesExtensionsTests
|
|||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("2.5")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithCoverImage("Chapter 2.5")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 2")
|
||||
.WithCoverImage("Chapter 2")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(true)
|
||||
.WithCoverImage("Special 3")
|
||||
.WithCoverImage("Special 1")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -107,11 +167,11 @@ public class SeriesExtensionsTests
|
|||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Special 2", series.GetCoverImage());
|
||||
Assert.Equal("Chapter 2", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_VolumesChapters_Comics()
|
||||
public void GetCoverImage_VolumesChapters()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
|
@ -119,11 +179,11 @@ public class SeriesExtensionsTests
|
|||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("2.5")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithCoverImage("Chapter 2.5")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 2")
|
||||
.WithCoverImage("Chapter 2")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(true)
|
||||
|
@ -148,7 +208,7 @@ public class SeriesExtensionsTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_VolumesChaptersAndSpecials_Comics()
|
||||
public void GetCoverImage_VolumesChaptersAndSpecials()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
|
@ -156,15 +216,15 @@ public class SeriesExtensionsTests
|
|||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("2.5")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithCoverImage("Chapter 2.5")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 2")
|
||||
.WithCoverImage("Chapter 2")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(true)
|
||||
.WithCoverImage("Special 3")
|
||||
.WithCoverImage("Special 1")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
|
@ -184,5 +244,82 @@ public class SeriesExtensionsTests
|
|||
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_VolumesChaptersAndSpecials_Ippo()
|
||||
{
|
||||
var series = new SeriesBuilder("Ippo")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("1426")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Chapter 1426")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("1425")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Chapter 1425")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(true)
|
||||
.WithCoverImage("Special 1")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 1")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("137")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 137")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_VolumesChapters_WhereVolumeIsNot1()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("2.5")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Chapter 2.5")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Chapter 2")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("4")
|
||||
.WithNumber(4)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 4")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||
}
|
||||
|
||||
Assert.Equal("Chapter 2", series.GetCoverImage());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
28
API.Tests/Extensions/SeriesFilterTests.cs
Normal file
28
API.Tests/Extensions/SeriesFilterTests.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.Extensions.QueryExtensions.Filtering;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Extensions;
|
||||
|
||||
public class SeriesFilterTests : AbstractDbTest
|
||||
{
|
||||
|
||||
protected override Task ResetDb()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#region HasLanguage
|
||||
|
||||
[Fact]
|
||||
public async Task HasLanguage_Works()
|
||||
{
|
||||
var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.Contains, new List<string>() { }).ToListAsync();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
|
@ -39,4 +39,5 @@ public class BookParserTests
|
|||
// var actual = API.Parser.Parser.CssImportUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3");
|
||||
// Assert.Equal(expected, actual);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -197,6 +197,7 @@ public class MangaParserTests
|
|||
[InlineData("Esquire 6권 2021년 10월호", "Esquire")]
|
||||
[InlineData("Accel World: Vol 1", "Accel World")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "Accel World")]
|
||||
[InlineData("Bleach 001-003", "Bleach")]
|
||||
public void ParseSeriesTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
||||
|
@ -281,6 +282,7 @@ public class MangaParserTests
|
|||
[InlineData("Манга 2 Глава", "2")]
|
||||
[InlineData("Манга Том 1 2 Глава", "2")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "1")]
|
||||
[InlineData("Bleach 001-003", "1-3")]
|
||||
public void ParseChaptersTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||
|
|
|
@ -225,6 +225,7 @@ public class ParserTests
|
|||
[InlineData("@Recently-Snapshot/Love Hina/", true)]
|
||||
[InlineData("@recycle/Love Hina/", true)]
|
||||
[InlineData("E:/Test/__MACOSX/Love Hina/", true)]
|
||||
[InlineData("E:/Test/.caltrash/Love Hina/", true)]
|
||||
public void HasBlacklistedFolderInPathTest(string inputPath, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, HasBlacklistedFolderInPath(inputPath));
|
||||
|
|
|
@ -5,7 +5,9 @@ using System.IO.Abstractions.TestingHelpers;
|
|||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using API.Archive;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
using NSubstitute;
|
||||
|
@ -26,7 +28,9 @@ public class ArchiveServiceTests
|
|||
public ArchiveServiceTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
_archiveService = new ArchiveService(_logger, _directoryService, new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService));
|
||||
_archiveService = new ArchiveService(_logger, _directoryService,
|
||||
new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService, Substitute.For<IEasyCachingProviderFactory>()),
|
||||
Substitute.For<IMediaErrorService>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -152,7 +156,7 @@ public class ArchiveServiceTests
|
|||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
//[Theory]
|
||||
//[InlineData("v10.cbz", "v10.expected.png")] // Commented out as these break usually when NetVips is updated
|
||||
//[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.png")]
|
||||
//[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.png")]
|
||||
|
@ -163,8 +167,8 @@ public class ArchiveServiceTests
|
|||
public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile)
|
||||
{
|
||||
var ds = Substitute.For<DirectoryService>(_directoryServiceLogger, new FileSystem());
|
||||
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), ds);
|
||||
var archiveService = Substitute.For<ArchiveService>(_logger, ds, imageService);
|
||||
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), ds, Substitute.For<IEasyCachingProviderFactory>());
|
||||
var archiveService = Substitute.For<ArchiveService>(_logger, ds, imageService, Substitute.For<IMediaErrorService>());
|
||||
|
||||
var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages"));
|
||||
var expectedBytes = Image.Thumbnail(Path.Join(testDirectory, expectedOutputFile), 320).WriteToBuffer(".png");
|
||||
|
@ -176,7 +180,7 @@ public class ArchiveServiceTests
|
|||
_directoryService.ExistOrCreate(outputDir);
|
||||
|
||||
var coverImagePath = archiveService.GetCoverImage(Path.Join(testDirectory, inputFile),
|
||||
Path.GetFileNameWithoutExtension(inputFile) + "_output", outputDir);
|
||||
Path.GetFileNameWithoutExtension(inputFile) + "_output", outputDir, EncodeFormat.PNG);
|
||||
var actual = File.ReadAllBytes(Path.Join(outputDir, coverImagePath));
|
||||
|
||||
|
||||
|
@ -185,7 +189,7 @@ public class ArchiveServiceTests
|
|||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
//[Theory]
|
||||
//[InlineData("v10.cbz", "v10.expected.png")] // Commented out as these break usually when NetVips is updated
|
||||
//[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.png")]
|
||||
//[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.png")]
|
||||
|
@ -194,9 +198,10 @@ public class ArchiveServiceTests
|
|||
[InlineData("sorting.zip", "sorting.expected.png")]
|
||||
public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile)
|
||||
{
|
||||
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService);
|
||||
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService, Substitute.For<IEasyCachingProviderFactory>());
|
||||
var archiveService = Substitute.For<ArchiveService>(_logger,
|
||||
new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService);
|
||||
new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService,
|
||||
Substitute.For<IMediaErrorService>());
|
||||
var testDirectory = API.Services.Tasks.Scanner.Parser.Parser.NormalizePath(Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages")));
|
||||
|
||||
var outputDir = Path.Join(testDirectory, "output");
|
||||
|
@ -205,7 +210,7 @@ public class ArchiveServiceTests
|
|||
|
||||
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.SharpCompress);
|
||||
var coverOutputFile = archiveService.GetCoverImage(Path.Join(testDirectory, inputFile),
|
||||
Path.GetFileNameWithoutExtension(inputFile), outputDir);
|
||||
Path.GetFileNameWithoutExtension(inputFile), outputDir, EncodeFormat.PNG);
|
||||
var actualBytes = File.ReadAllBytes(Path.Join(outputDir, coverOutputFile));
|
||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||
Assert.Equal(expectedBytes, actualBytes);
|
||||
|
@ -219,13 +224,14 @@ public class ArchiveServiceTests
|
|||
public void CanParseCoverImage(string inputFile)
|
||||
{
|
||||
var imageService = Substitute.For<IImageService>();
|
||||
imageService.WriteCoverThumbnail(Arg.Any<Stream>(), Arg.Any<string>(), Arg.Any<string>()).Returns(x => "cover.jpg");
|
||||
var archiveService = new ArchiveService(_logger, _directoryService, imageService);
|
||||
imageService.WriteCoverThumbnail(Arg.Any<Stream>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<EncodeFormat>())
|
||||
.Returns(x => "cover.jpg");
|
||||
var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For<IMediaErrorService>());
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/");
|
||||
var inputPath = Path.GetFullPath(Path.Join(testDirectory, inputFile));
|
||||
var outputPath = Path.Join(testDirectory, Path.GetFileNameWithoutExtension(inputFile) + "_output");
|
||||
new DirectoryInfo(outputPath).Create();
|
||||
var expectedImage = archiveService.GetCoverImage(inputPath, inputFile, outputPath);
|
||||
var expectedImage = archiveService.GetCoverImage(inputPath, inputFile, outputPath, EncodeFormat.PNG);
|
||||
Assert.Equal("cover.jpg", expectedImage);
|
||||
new DirectoryInfo(outputPath).Delete();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using API.Services;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
@ -15,7 +16,9 @@ public class BookServiceTests
|
|||
public BookServiceTests()
|
||||
{
|
||||
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem());
|
||||
_bookService = new BookService(_logger, directoryService, new ImageService(Substitute.For<ILogger<ImageService>>(), directoryService));
|
||||
_bookService = new BookService(_logger, directoryService,
|
||||
new ImageService(Substitute.For<ILogger<ImageService>>(), directoryService, Substitute.For<IEasyCachingProviderFactory>())
|
||||
, Substitute.For<IMediaErrorService>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
@ -55,7 +55,7 @@ public class BookmarkServiceTests
|
|||
private BookmarkService Create(IDirectoryService ds)
|
||||
{
|
||||
return new BookmarkService(Substitute.For<ILogger<BookmarkService>>(), _unitOfWork, ds,
|
||||
Substitute.For<IImageService>(), Substitute.For<IEventHub>());
|
||||
Substitute.For<IMediaConversionService>());
|
||||
}
|
||||
|
||||
#region Setup
|
||||
|
|
|
@ -42,7 +42,7 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService
|
|||
return 1;
|
||||
}
|
||||
|
||||
public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, bool saveAsWebP)
|
||||
public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ using API.Extensions;
|
|||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -38,7 +39,7 @@ public class CleanupServiceTests : AbstractDbTest
|
|||
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>(),
|
||||
Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()), Substitute.For<IScrobblingService>());
|
||||
}
|
||||
|
||||
#region Setup
|
||||
|
|
|
@ -43,7 +43,7 @@ internal class MockReadingItemService : IReadingItemService
|
|||
return 1;
|
||||
}
|
||||
|
||||
public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, bool saveAsWebP)
|
||||
public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
|
|
@ -14,10 +14,13 @@ using API.Extensions;
|
|||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using AutoMapper;
|
||||
using Hangfire;
|
||||
using Hangfire.InMemory;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -52,7 +55,8 @@ public class ReaderServiceTests
|
|||
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()),
|
||||
Substitute.For<IScrobblingService>());
|
||||
}
|
||||
|
||||
#region Setup
|
||||
|
@ -146,8 +150,8 @@ public class ReaderServiceTests
|
|||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
Assert.Equal(0, await _readerService.CapPageToChapter(1, -1));
|
||||
Assert.Equal(1, await _readerService.CapPageToChapter(1, 10));
|
||||
Assert.Equal(0, (await _readerService.CapPageToChapter(1, -1)).Item1);
|
||||
Assert.Equal(1, (await _readerService.CapPageToChapter(1, 10)).Item1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -179,7 +183,7 @@ public class ReaderServiceTests
|
|||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
JobStorage.Current = new InMemoryStorage();
|
||||
var successful = await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
ChapterId = 1,
|
||||
|
@ -217,8 +221,7 @@ public class ReaderServiceTests
|
|||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
JobStorage.Current = new InMemoryStorage();
|
||||
var successful = await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
ChapterId = 1,
|
||||
|
@ -378,6 +381,49 @@ public class ReaderServiceTests
|
|||
Assert.Equal("2", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldGetNextVolume_OnlyFloats()
|
||||
{
|
||||
// V1 -> V2
|
||||
await ResetDb();
|
||||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1.0")
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2.1")
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2.2")
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3.1")
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.Build())
|
||||
|
||||
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("31", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume()
|
||||
{
|
||||
|
@ -456,8 +502,6 @@ public class ReaderServiceTests
|
|||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("21", actualChapter.Range);
|
||||
|
@ -492,9 +536,6 @@ public class ReaderServiceTests
|
|||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
|
@ -502,7 +543,7 @@ public class ReaderServiceTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldRollIntoNextChapterWhenVolumesAreOnlyOneChapterAndNextChapterIs0()
|
||||
public async Task GetNextChapterIdAsync_ShouldRollIntoNextChapter_WhenVolumesAreOnlyOneChapter_AndNextChapterIs0()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
|
@ -564,9 +605,6 @@ public class ReaderServiceTests
|
|||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
|
@ -574,9 +612,6 @@ public class ReaderServiceTests
|
|||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
@ -596,7 +631,6 @@ public class ReaderServiceTests
|
|||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
|
@ -604,9 +638,6 @@ public class ReaderServiceTests
|
|||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
@ -622,18 +653,10 @@ public class ReaderServiceTests
|
|||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
|
@ -641,13 +664,45 @@ public class ReaderServiceTests
|
|||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
||||
// This is commented out because, while valid, I can't solve how to make this pass
|
||||
// [Fact]
|
||||
// public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials()
|
||||
// {
|
||||
// await ResetDb();
|
||||
//
|
||||
// var series = new SeriesBuilder("Test")
|
||||
// .WithVolume(new VolumeBuilder("0")
|
||||
// .WithNumber(0)
|
||||
// .WithChapter(new ChapterBuilder("1").Build())
|
||||
// .WithChapter(new ChapterBuilder("2").Build())
|
||||
// .WithChapter(new ChapterBuilder("0").WithIsSpecial(true).Build())
|
||||
// .Build())
|
||||
//
|
||||
// .WithVolume(new VolumeBuilder("1")
|
||||
// .WithNumber(1)
|
||||
// .WithChapter(new ChapterBuilder("2").Build())
|
||||
// .Build())
|
||||
// .Build();
|
||||
// series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
//
|
||||
// _context.Series.Add(series);
|
||||
// _context.AppUser.Add(new AppUser()
|
||||
// {
|
||||
// UserName = "majora2007"
|
||||
// });
|
||||
//
|
||||
// await _context.SaveChangesAsync();
|
||||
//
|
||||
// var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
// Assert.Equal(-1, nextChapter);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldMoveFromVolumeToSpecial_NoLooseLeafChapters()
|
||||
{
|
||||
|
@ -1663,6 +1718,59 @@ public class ReaderServiceTests
|
|||
Assert.Equal("1", nextChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnLooseChapter_WhenAllVolumesRead_HasSpecialAndLooseChapters_Unread()
|
||||
{
|
||||
await ResetDb();
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("100").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("101").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithPages(1).Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("0").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("0").WithPages(1).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
var user = new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
};
|
||||
_context.AppUser.Add(user);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Mark everything but chapter 101 as read
|
||||
await _readerService.MarkSeriesAsRead(user, 1);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Unmark last chapter as read
|
||||
var vol = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1);
|
||||
foreach (var chapt in vol.Chapters)
|
||||
{
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 0,
|
||||
ChapterId = chapt.Id,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("100", nextChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnLooseChapter_WhenAllVolumesAndAFewLooseChaptersRead()
|
||||
{
|
||||
|
@ -1694,24 +1802,23 @@ public class ReaderServiceTests
|
|||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
// Mark everything but chapter 101 as read
|
||||
await _readerService.MarkSeriesAsRead(user, 1);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Unmark last chapter as read
|
||||
var vol = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1);
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 0,
|
||||
ChapterId = (await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1)).Chapters.ElementAt(1).Id,
|
||||
ChapterId = vol.Chapters.ElementAt(1).Id,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 0,
|
||||
ChapterId = (await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1)).Chapters.ElementAt(2).Id,
|
||||
ChapterId = vol.Chapters.ElementAt(2).Id,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
|
@ -1986,6 +2093,184 @@ public class ReaderServiceTests
|
|||
Assert.Equal(4, nextChapter.VolumeId);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Volume 1-10 are fully read (single volumes),
|
||||
/// Special 1 is fully read
|
||||
/// Chapters 56-90 are read
|
||||
/// Chapter 91 has partial progress on
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnLastLooseChapter()
|
||||
{
|
||||
await ResetDb();
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("22").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("51").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("52").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("91").WithPages(2).Build())
|
||||
.WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithPages(1).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 1,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 2,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 3,
|
||||
SeriesId = 1,
|
||||
VolumeId = 2
|
||||
}, 1);
|
||||
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 4,
|
||||
SeriesId = 1,
|
||||
VolumeId = 2
|
||||
}, 1);
|
||||
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 5,
|
||||
SeriesId = 1,
|
||||
VolumeId = 2
|
||||
}, 1);
|
||||
|
||||
// Chapter 91 has partial progress, hence it should resume there
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 6,
|
||||
SeriesId = 1,
|
||||
VolumeId = 2
|
||||
}, 1);
|
||||
|
||||
// Special is fully read
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 7,
|
||||
SeriesId = 1,
|
||||
VolumeId = 2
|
||||
}, 1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("91", nextChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_DuplicateIssueNumberBetweenChapters()
|
||||
{
|
||||
await ResetDb();
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("2").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("22").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("2").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("22").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 1,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("2", nextChapter.Range);
|
||||
Assert.Equal(1, nextChapter.VolumeId);
|
||||
|
||||
// Mark chapter 2 as read
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 2,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("21", nextChapter.Range);
|
||||
Assert.Equal(1, nextChapter.VolumeId);
|
||||
|
||||
// Mark chapter 21 as read
|
||||
await _readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 3,
|
||||
SeriesId = 1,
|
||||
VolumeId = 1
|
||||
}, 1);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("22", nextChapter.Range);
|
||||
Assert.Equal(1, nextChapter.VolumeId);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region MarkChaptersUntilAsRead
|
||||
|
|
|
@ -16,6 +16,7 @@ using API.Extensions;
|
|||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
|
@ -55,7 +56,8 @@ public class ReadingListServiceTests
|
|||
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()),
|
||||
Substitute.For<IScrobblingService>());
|
||||
}
|
||||
|
||||
#region Setup
|
||||
|
@ -804,7 +806,7 @@ public class ReadingListServiceTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Equal("A list of this name already exists", ex.Message);
|
||||
Assert.Equal("reading-list-name-exists", ex.Message);
|
||||
}
|
||||
Assert.Single((await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.ReadingLists))
|
||||
.ReadingLists);
|
||||
|
@ -832,7 +834,7 @@ public class ReadingListServiceTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Equal("A list of this name already exists", ex.Message);
|
||||
Assert.Equal("reading-list-name-exists", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -858,7 +860,7 @@ public class ReadingListServiceTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Equal("A list of this name already exists", ex.Message);
|
||||
Assert.Equal("reading-list-name-exists", ex.Message);
|
||||
}
|
||||
Assert.Single((await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.ReadingLists))
|
||||
.ReadingLists);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
|
@ -14,22 +15,53 @@ using API.Entities.Metadata;
|
|||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using Hangfire;
|
||||
using Hangfire.InMemory;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Hosting.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
||||
internal class MockHostingEnvironment : IHostEnvironment {
|
||||
public string ApplicationName { get => "API"; set => throw new NotImplementedException(); }
|
||||
public IFileProvider ContentRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
|
||||
public string ContentRootPath
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public string EnvironmentName { get => "Testing"; set => throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
|
||||
public class SeriesServiceTests : AbstractDbTest
|
||||
{
|
||||
private readonly ISeriesService _seriesService;
|
||||
|
||||
public SeriesServiceTests() : base()
|
||||
{
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem()
|
||||
{
|
||||
|
||||
});
|
||||
|
||||
|
||||
var locService = new LocalizationService(ds, new MockHostingEnvironment(),
|
||||
Substitute.For<IMemoryCache>(), Substitute.For<IUnitOfWork>());
|
||||
|
||||
_seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
|
||||
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>());
|
||||
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>(),
|
||||
Substitute.For<IScrobblingService>(), locService);
|
||||
}
|
||||
#region Setup
|
||||
|
||||
|
@ -334,20 +366,19 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings);
|
||||
|
||||
JobStorage.Current = new InMemoryStorage();
|
||||
var result = await _seriesService.UpdateRating(user, new UpdateSeriesRatingDto()
|
||||
{
|
||||
SeriesId = 1,
|
||||
UserRating = 3,
|
||||
UserReview = "Average"
|
||||
});
|
||||
|
||||
Assert.True(result);
|
||||
|
||||
var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings))
|
||||
var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings))!
|
||||
.Ratings;
|
||||
Assert.NotEmpty(ratings);
|
||||
Assert.Equal(3, ratings.First().Rating);
|
||||
Assert.Equal("Average", ratings.First().Review);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -374,16 +405,15 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
{
|
||||
SeriesId = 1,
|
||||
UserRating = 3,
|
||||
UserReview = "Average"
|
||||
});
|
||||
|
||||
Assert.True(result);
|
||||
|
||||
JobStorage.Current = new InMemoryStorage();
|
||||
var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings))
|
||||
.Ratings;
|
||||
Assert.NotEmpty(ratings);
|
||||
Assert.Equal(3, ratings.First().Rating);
|
||||
Assert.Equal("Average", ratings.First().Review);
|
||||
|
||||
// Update the DB again
|
||||
|
||||
|
@ -391,7 +421,6 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
{
|
||||
SeriesId = 1,
|
||||
UserRating = 5,
|
||||
UserReview = "Average"
|
||||
});
|
||||
|
||||
Assert.True(result2);
|
||||
|
@ -401,7 +430,6 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
Assert.NotEmpty(ratings2);
|
||||
Assert.True(ratings2.Count == 1);
|
||||
Assert.Equal(5, ratings2.First().Rating);
|
||||
Assert.Equal("Average", ratings2.First().Review);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -427,16 +455,16 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
{
|
||||
SeriesId = 1,
|
||||
UserRating = 10,
|
||||
UserReview = "Average"
|
||||
});
|
||||
|
||||
Assert.True(result);
|
||||
|
||||
var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings))
|
||||
JobStorage.Current = new InMemoryStorage();
|
||||
var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007",
|
||||
AppUserIncludes.Ratings))
|
||||
.Ratings;
|
||||
Assert.NotEmpty(ratings);
|
||||
Assert.Equal(5, ratings.First().Rating);
|
||||
Assert.Equal("Average", ratings.First().Review);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -462,7 +490,6 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
{
|
||||
SeriesId = 2,
|
||||
UserRating = 5,
|
||||
UserReview = "Average"
|
||||
});
|
||||
|
||||
Assert.False(result);
|
||||
|
@ -775,12 +802,32 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
return series;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFirstChapterForMetadata_BookWithOnlyVolumeNumbers_Test()
|
||||
{
|
||||
var file = new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build();
|
||||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("0").WithPages(1).WithFile(file).Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1.5")
|
||||
.WithChapter(new ChapterBuilder("0").WithPages(2).WithFile(file).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build();
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.Equal(1, firstChapter.Pages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFirstChapterForMetadata_Book_Test()
|
||||
{
|
||||
var series = CreateSeriesMock();
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series, true);
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.Same("1", firstChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -789,7 +836,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
{
|
||||
var series = CreateSeriesMock();
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series, false);
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.Same("1", firstChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -808,10 +855,35 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
new ChapterBuilder("1.2").WithFiles(files).WithPages(1).Build(),
|
||||
};
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series, false);
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.Same("1.1", firstChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFirstChapterForMetadata_NonBook_ShouldReturnChapter1_WhenFirstVolumeIs3()
|
||||
{
|
||||
var file = new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build();
|
||||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).WithFile(file).Build())
|
||||
.WithChapter(new ChapterBuilder("2").WithPages(1).WithFile(file).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).WithFile(file).Build())
|
||||
.WithChapter(new ChapterBuilder("22").WithPages(1).WithFile(file).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).WithFile(file).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).WithFile(file).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build();
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.Same("1", firstChapter.Range);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SeriesRelation
|
||||
|
@ -1170,9 +1242,19 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
[InlineData(LibraryType.Comic, false, "Issue")]
|
||||
[InlineData(LibraryType.Comic, true, "Issue #")]
|
||||
[InlineData(LibraryType.Book, false, "Book")]
|
||||
public void FormatChapterNameTest(LibraryType libraryType, bool withHash, string expected )
|
||||
public async Task FormatChapterNameTest(LibraryType libraryType, bool withHash, string expected )
|
||||
{
|
||||
Assert.Equal(expected, SeriesService.FormatChapterName(libraryType, withHash));
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
Assert.Equal(expected, await _seriesService.FormatChapterName(1, libraryType, withHash));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -1180,59 +1262,132 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
#region FormatChapterTitle
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterTitle_Manga_NonSpecial()
|
||||
public async Task FormatChapterTitle_Manga_NonSpecial()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build();
|
||||
Assert.Equal("Chapter Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Manga, false));
|
||||
Assert.Equal("Chapter Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterTitle_Manga_Special()
|
||||
public async Task FormatChapterTitle_Manga_Special()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build();
|
||||
Assert.Equal("Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Manga, false));
|
||||
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterTitle_Comic_NonSpecial_WithoutHash()
|
||||
public async Task FormatChapterTitle_Comic_NonSpecial_WithoutHash()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build();
|
||||
Assert.Equal("Issue Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Comic, false));
|
||||
Assert.Equal("Issue Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterTitle_Comic_Special_WithoutHash()
|
||||
public async Task FormatChapterTitle_Comic_Special_WithoutHash()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build();
|
||||
Assert.Equal("Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Comic, false));
|
||||
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterTitle_Comic_NonSpecial_WithHash()
|
||||
public async Task FormatChapterTitle_Comic_NonSpecial_WithHash()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build();
|
||||
Assert.Equal("Issue #Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Comic, true));
|
||||
Assert.Equal("Issue #Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterTitle_Comic_Special_WithHash()
|
||||
public async Task FormatChapterTitle_Comic_Special_WithHash()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build();
|
||||
Assert.Equal("Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Comic, true));
|
||||
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterTitle_Book_NonSpecial()
|
||||
public async Task FormatChapterTitle_Book_NonSpecial()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build();
|
||||
Assert.Equal("Book Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Book, false));
|
||||
Assert.Equal("Book Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatChapterTitle_Book_Special()
|
||||
public async Task FormatChapterTitle_Book_Special()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
|
||||
.WithLocale("en")
|
||||
.Build())
|
||||
.Build());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build();
|
||||
Assert.Equal("Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Book, false));
|
||||
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
@ -49,7 +50,8 @@ public class TachiyomiServiceTests
|
|||
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()),
|
||||
Substitute.For<IScrobblingService>());
|
||||
_tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), _readerService);
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.Entities.Enums;
|
|||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks;
|
||||
using API.Services.Tasks.Metadata;
|
||||
using API.SignalR;
|
||||
|
@ -23,15 +24,16 @@ public class WordCountAnalysisTests : AbstractDbTest
|
|||
{
|
||||
private readonly IReaderService _readerService;
|
||||
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService");
|
||||
private const long WordCount = 37417;
|
||||
private const long WordCount = 33608; // 37417 if splitting on space, 33608 if just character count
|
||||
private const long MinHoursToRead = 1;
|
||||
private const long AvgHoursToRead = 2;
|
||||
private const long MaxHoursToRead = 4;
|
||||
private const long MaxHoursToRead = 3;
|
||||
public WordCountAnalysisTests() : base()
|
||||
{
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()),
|
||||
Substitute.For<IScrobblingService>());
|
||||
}
|
||||
|
||||
protected override async Task ResetDb()
|
||||
|
@ -146,8 +148,8 @@ public class WordCountAnalysisTests : AbstractDbTest
|
|||
|
||||
Assert.Equal(WordCount * 2L, series.WordCount);
|
||||
Assert.Equal(MinHoursToRead * 2, series.MinHoursToRead);
|
||||
Assert.Equal(AvgHoursToRead * 2, series.AvgHoursToRead);
|
||||
Assert.Equal((MaxHoursToRead * 2) - 1, series.MaxHoursToRead); // This is just a rounding issue
|
||||
//Assert.Equal(AvgHoursToRead * 2, series.AvgHoursToRead);
|
||||
//Assert.Equal((MaxHoursToRead * 2) - 1, series.MaxHoursToRead); // This is just a rounding issue
|
||||
|
||||
var firstVolume = series.Volumes.ElementAt(0);
|
||||
Assert.Equal(WordCount, firstVolume.WordCount);
|
||||
|
|
|
@ -56,53 +56,55 @@
|
|||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.4" />
|
||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||
<PackageReference Include="EasyCaching.InMemory" Version="1.9.0" />
|
||||
<PackageReference Include="ExCSS" Version="4.2.1" />
|
||||
<PackageReference Include="Flurl" Version="3.0.7" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Hangfire" Version="1.7.34" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.34" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.3.7" />
|
||||
<PackageReference Include="Hangfire" Version="1.8.4" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.4" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.5.1" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.3" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.51" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
<PackageReference Include="NetVips" Version="2.3.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.14.2" />
|
||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||
<PackageReference Include="NetVips" Version="2.3.1" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.14.3" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.6" />
|
||||
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
|
||||
<PackageReference Include="Serilog" Version="3.0.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.55.0.65544">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.7.0.75501">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.29.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="19.2.11" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.8" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.1" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="19.2.51" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -189,6 +191,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Folder Include="config\themes" />
|
||||
<Folder Include="I18N\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
25
API/Constants/CacheProfiles.cs
Normal file
25
API/Constants/CacheProfiles.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
namespace API.Constants;
|
||||
|
||||
public static class EasyCacheProfiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Not in use
|
||||
/// </summary>
|
||||
public const string RevokedJwt = "revokedJWT";
|
||||
public const string Favicon = "favicon";
|
||||
/// <summary>
|
||||
/// If a user's license is valid
|
||||
/// </summary>
|
||||
public const string License = "license";
|
||||
/// <summary>
|
||||
/// Cache the libraries on the server
|
||||
/// </summary>
|
||||
public const string Library = "library";
|
||||
/// <summary>
|
||||
/// Metadata filter
|
||||
/// </summary>
|
||||
public const string Filter = "filter";
|
||||
public const string KavitaPlusReviews = "kavita+reviews";
|
||||
public const string KavitaPlusRecommendations = "kavita+recommendations";
|
||||
public const string KavitaPlusRatings = "kavita+ratings";
|
||||
}
|
6
API/Constants/ControllerConstants.cs
Normal file
6
API/Constants/ControllerConstants.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace API.Constants;
|
||||
|
||||
public abstract class ControllerConstants
|
||||
{
|
||||
public const int MaxUploadSizeBytes = 8_000_000;
|
||||
}
|
|
@ -15,4 +15,6 @@ public static class ResponseCacheProfiles
|
|||
/// </summary>
|
||||
public const string Instant = "Instant";
|
||||
public const string Month = "Month";
|
||||
public const string LicenseCache = "LicenseCache";
|
||||
public const string KavitaPlus = "KavitaPlus";
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ using API.Entities.Enums;
|
|||
using API.Errors;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Middleware.RateLimit;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
|
@ -44,6 +43,7 @@ public class AccountController : BaseApiController
|
|||
private readonly IAccountService _accountService;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public AccountController(UserManager<AppUser> userManager,
|
||||
|
@ -51,7 +51,8 @@ public class AccountController : BaseApiController
|
|||
ITokenService tokenService, IUnitOfWork unitOfWork,
|
||||
ILogger<AccountController> logger,
|
||||
IMapper mapper, IAccountService accountService,
|
||||
IEmailService emailService, IEventHub eventHub)
|
||||
IEmailService emailService, IEventHub eventHub,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
|
@ -62,6 +63,7 @@ public class AccountController : BaseApiController
|
|||
_accountService = accountService;
|
||||
_emailService = emailService;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -69,32 +71,31 @@ public class AccountController : BaseApiController
|
|||
/// </summary>
|
||||
/// <param name="resetPasswordDto"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
||||
{
|
||||
// TODO: Log this request to Audit Table
|
||||
_logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName);
|
||||
|
||||
var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName);
|
||||
if (user == null) return Ok(); // Don't report BadRequest as that would allow brute forcing to find accounts on system
|
||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||
|
||||
|
||||
if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin))
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (resetPasswordDto.UserName != User.GetUsername() && !isAdmin)
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (string.IsNullOrEmpty(resetPasswordDto.OldPassword) && !isAdmin)
|
||||
return BadRequest(new ApiException(400, "You must enter your existing password to change your account unless you're an admin"));
|
||||
return BadRequest(
|
||||
new ApiException(400,
|
||||
await _localizationService.Translate(User.GetUserId(), "password-required")));
|
||||
|
||||
// If you're an admin and the username isn't yours, you don't need to validate the password
|
||||
var isResettingOtherUser = (resetPasswordDto.UserName != User.GetUsername() && isAdmin);
|
||||
if (!isResettingOtherUser && !await _userManager.CheckPasswordAsync(user, resetPasswordDto.OldPassword))
|
||||
{
|
||||
return BadRequest("Invalid Password");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-password"));
|
||||
}
|
||||
|
||||
var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password);
|
||||
|
@ -117,7 +118,7 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<UserDto>> RegisterFirstUser(RegisterDto registerDto)
|
||||
{
|
||||
var admins = await _userManager.GetUsersInRoleAsync("Admin");
|
||||
if (admins.Count > 0) return BadRequest("Not allowed");
|
||||
if (admins.Count > 0) return BadRequest(await _localizationService.Get("en", "denied"));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -135,8 +136,8 @@ public class AccountController : BaseApiController
|
|||
if (!result.Succeeded) return BadRequest(result.Errors);
|
||||
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue generating a confirmation token.");
|
||||
if (!await ConfirmEmailToken(token, user)) return BadRequest($"There was an issue validating your email: {token}");
|
||||
if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen"));
|
||||
if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "validate-email", token));
|
||||
|
||||
|
||||
var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole);
|
||||
|
@ -151,7 +152,7 @@ public class AccountController : BaseApiController
|
|||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences),
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -163,7 +164,7 @@ public class AccountController : BaseApiController
|
|||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Something went wrong when registering user");
|
||||
return BadRequest(await _localizationService.Get("en", "register-user"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -176,25 +177,40 @@ public class AccountController : BaseApiController
|
|||
[HttpPost("login")]
|
||||
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
||||
{
|
||||
var user = await _userManager.Users
|
||||
.Include(u => u.UserPreferences)
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
|
||||
|
||||
if (user == null) return Unauthorized("Your credentials are not correct");
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized("Your account is disabled. Contact the server admin.");
|
||||
|
||||
var result = await _signInManager
|
||||
.CheckPasswordSignInAsync(user, loginDto.Password, true);
|
||||
|
||||
if (result.IsLockedOut)
|
||||
AppUser? user;
|
||||
if (!string.IsNullOrEmpty(loginDto.ApiKey))
|
||||
{
|
||||
return Unauthorized("You've been locked out from too many authorization attempts. Please wait 10 minutes.");
|
||||
user = await _userManager.Users
|
||||
.Include(u => u.UserPreferences)
|
||||
.SingleOrDefaultAsync(x => x.ApiKey == loginDto.ApiKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
user = await _userManager.Users
|
||||
.Include(u => u.UserPreferences)
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
|
||||
}
|
||||
|
||||
if (!result.Succeeded)
|
||||
|
||||
if (user == null) return Unauthorized(await _localizationService.Get("en", "bad-credentials"));
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account"));
|
||||
|
||||
if (string.IsNullOrEmpty(loginDto.ApiKey))
|
||||
{
|
||||
return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct");
|
||||
var result = await _signInManager
|
||||
.CheckPasswordSignInAsync(user, loginDto.Password, true);
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
await _userManager.UpdateSecurityStampAsync(user);
|
||||
return Unauthorized(await _localizationService.Translate(user.Id, "locked-out"));
|
||||
}
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return Unauthorized(await _localizationService.Translate(user.Id, result.IsNotAllowed ? "confirm-email" : "bad-credentials"));
|
||||
}
|
||||
}
|
||||
|
||||
// Update LastActive on account
|
||||
|
@ -225,6 +241,24 @@ public class AccountController : BaseApiController
|
|||
return Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an up-to-date user account
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("refresh-account")]
|
||||
public async Task<ActionResult<UserDto>> RefreshAccount()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.UserPreferences);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var dto = _mapper.Map<UserDto>(user);
|
||||
dto.Token = await _tokenService.CreateToken(user);
|
||||
dto.RefreshToken = await _tokenService.CreateRefreshToken(user);
|
||||
dto.KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion))
|
||||
.Value;
|
||||
return Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the user's JWT token
|
||||
/// </summary>
|
||||
|
@ -237,7 +271,7 @@ public class AccountController : BaseApiController
|
|||
var token = await _tokenService.ValidateRefreshToken(tokenRequestDto);
|
||||
if (token == null)
|
||||
{
|
||||
return Unauthorized(new { message = "Invalid token" });
|
||||
return Unauthorized(new { message = await _localizationService.Get("en", "invalid-token") });
|
||||
}
|
||||
|
||||
return Ok(token);
|
||||
|
@ -276,7 +310,7 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return BadRequest("Something went wrong, unable to reset key");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "unable-to-reset-key"));
|
||||
|
||||
}
|
||||
|
||||
|
@ -291,26 +325,27 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized("You do not have permission");
|
||||
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password)) return BadRequest("Invalid payload");
|
||||
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
||||
|
||||
|
||||
// Validate this user's password
|
||||
if (! await _userManager.CheckPasswordAsync(user, dto.Password))
|
||||
{
|
||||
_logger.LogCritical("A user tried to change {UserName}'s email, but password didn't validate", user.UserName);
|
||||
return BadRequest("You do not have permission");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
}
|
||||
|
||||
// Validate no other users exist with this email
|
||||
if (user.Email!.Equals(dto.Email)) return Ok("Nothing to do");
|
||||
if (user.Email!.Equals(dto.Email)) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
// Check if email is used by another user
|
||||
var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (existingUserEmail != null)
|
||||
{
|
||||
return BadRequest("You cannot share emails across multiple accounts");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "share-multiple-emails"));
|
||||
}
|
||||
|
||||
// All validations complete, generate a new token and email it to the user at the new address. Confirm email link will update the email
|
||||
|
@ -318,7 +353,7 @@ public class AccountController : BaseApiController
|
|||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogError("There was an issue generating a token for the email");
|
||||
return BadRequest("There was an issue creating a confirmation email token. See logs.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token"));
|
||||
}
|
||||
|
||||
user.EmailConfirmed = false;
|
||||
|
@ -373,10 +408,10 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult> UpdateAgeRestriction(UpdateAgeRestrictionDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized("You do not have permission");
|
||||
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest("You do not have permission");
|
||||
if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
|
||||
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
|
||||
|
@ -391,7 +426,7 @@ public class AccountController : BaseApiController
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an error updating the age restriction");
|
||||
return BadRequest("There was an error updating the age restriction");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "age-restriction-update"));
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
|
@ -410,17 +445,17 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (adminUser == null) return Unauthorized();
|
||||
if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized("You do not have permission");
|
||||
if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId);
|
||||
if (user == null) return BadRequest("User does not exist");
|
||||
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-user"));
|
||||
|
||||
// Check if username is changing
|
||||
if (!user.UserName!.Equals(dto.Username))
|
||||
{
|
||||
// Validate username change
|
||||
var errors = await _accountService.ValidateUsername(dto.Username);
|
||||
if (errors.Any()) return BadRequest("Username already taken");
|
||||
if (errors.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "username-taken"));
|
||||
user.UserName = dto.Username;
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
}
|
||||
|
@ -443,6 +478,9 @@ public class AccountController : BaseApiController
|
|||
if (!roleResult.Succeeded) return BadRequest(roleResult.Errors);
|
||||
}
|
||||
|
||||
// We might want to check if they had admin and no longer, if so:
|
||||
// await _userManager.UpdateSecurityStampAsync(user); to force them to re-authenticate
|
||||
|
||||
|
||||
var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
||||
List<Library> libraries;
|
||||
|
@ -482,7 +520,7 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return BadRequest("There was an exception when updating the user");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-update"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -498,9 +536,9 @@ public class AccountController : BaseApiController
|
|||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.EmailConfirmed)
|
||||
return BadRequest("User is already confirmed");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-confirmed"));
|
||||
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
||||
return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "manual-setup-fail"));
|
||||
|
||||
return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl);
|
||||
}
|
||||
|
@ -517,7 +555,7 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
||||
{
|
||||
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (adminUser == null) return Unauthorized("You are not permitted");
|
||||
if (adminUser == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
||||
|
||||
|
@ -530,8 +568,8 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||
return BadRequest($"User is already registered as {invitedUser!.UserName}");
|
||||
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName));
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -586,7 +624,7 @@ public class AccountController : BaseApiController
|
|||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogError("There was an issue generating a token for the email");
|
||||
return BadRequest("There was an creating the invite user");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
|
||||
}
|
||||
|
||||
user.ConfirmationToken = token;
|
||||
|
@ -628,7 +666,7 @@ public class AccountController : BaseApiController
|
|||
_logger.LogError(ex, "There was an error during invite user flow, unable to send an email");
|
||||
}
|
||||
|
||||
return BadRequest("There was an error setting up your account. Please check the logs");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -645,7 +683,7 @@ public class AccountController : BaseApiController
|
|||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
|
||||
return BadRequest("Invalid email confirmation");
|
||||
return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation"));
|
||||
}
|
||||
|
||||
// Validate Password and Username
|
||||
|
@ -666,7 +704,7 @@ public class AccountController : BaseApiController
|
|||
if (!await ConfirmEmailToken(dto.Token, user))
|
||||
{
|
||||
_logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
|
||||
return BadRequest("Invalid email confirmation");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation"));
|
||||
}
|
||||
|
||||
user.UserName = dto.Username;
|
||||
|
@ -691,7 +729,7 @@ public class AccountController : BaseApiController
|
|||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences),
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -709,13 +747,13 @@ public class AccountController : BaseApiController
|
|||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
|
||||
return BadRequest("Invalid email confirmation");
|
||||
return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation"));
|
||||
}
|
||||
|
||||
if (!await ConfirmEmailToken(dto.Token, user))
|
||||
{
|
||||
_logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
|
||||
return BadRequest("Invalid email confirmation");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation"));
|
||||
}
|
||||
|
||||
_logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email);
|
||||
|
@ -723,7 +761,7 @@ public class AccountController : BaseApiController
|
|||
if (!result.Succeeded)
|
||||
{
|
||||
_logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description));
|
||||
return BadRequest("Unable to update email for user. Check logs");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "generic-user-email-update"));
|
||||
}
|
||||
user.ConfirmationToken = null;
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -741,12 +779,12 @@ public class AccountController : BaseApiController
|
|||
[HttpPost("confirm-password-reset")]
|
||||
public async Task<ActionResult<string>> ConfirmForgotPassword(ConfirmPasswordResetDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
try
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("Invalid credentials");
|
||||
return BadRequest(await _localizationService.Get("en", "bad-credentials"));
|
||||
}
|
||||
|
||||
var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider,
|
||||
|
@ -754,16 +792,16 @@ public class AccountController : BaseApiController
|
|||
if (!result)
|
||||
{
|
||||
_logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto);
|
||||
return BadRequest("Invalid credentials");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "bad-credentials"));
|
||||
}
|
||||
|
||||
var errors = await _accountService.ChangeUserPassword(user, dto.Password);
|
||||
return errors.Any() ? BadRequest(errors) : Ok("Password updated");
|
||||
return errors.Any() ? BadRequest(errors) : Ok(await _localizationService.Translate(user.Id, "password-updated"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an unexpected error when confirming new password");
|
||||
return BadRequest("There was an unexpected error when confirming new password");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "generic-password-update"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -782,15 +820,15 @@ public class AccountController : BaseApiController
|
|||
if (user == null)
|
||||
{
|
||||
_logger.LogError("There are no users with email: {Email} but user is requesting password reset", email);
|
||||
return Ok("An email will be sent to the email if it exists in our database");
|
||||
return Ok(await _localizationService.Get("en", "forgot-password-generic"));
|
||||
}
|
||||
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied"));
|
||||
|
||||
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
|
||||
return BadRequest("You do not have an email on account or it has not been confirmed");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "confirm-email"));
|
||||
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
|
||||
|
@ -803,10 +841,10 @@ public class AccountController : BaseApiController
|
|||
ServerConfirmationLink = emailLink,
|
||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||
});
|
||||
return Ok("Email sent");
|
||||
return Ok(await _localizationService.Translate(user.Id, "email-sent"));
|
||||
}
|
||||
|
||||
return Ok("Your server is not accessible. The Link to reset your password is in the logs.");
|
||||
return Ok(await _localizationService.Translate(user.Id, "not-accessible-password"));
|
||||
}
|
||||
|
||||
[HttpGet("email-confirmed")]
|
||||
|
@ -823,12 +861,12 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<UserDto>> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (user == null) return BadRequest("Invalid credentials");
|
||||
if (user == null) return BadRequest(await _localizationService.Get("en", "bad-credentials"));
|
||||
|
||||
if (!await ConfirmEmailToken(dto.Token, user))
|
||||
{
|
||||
_logger.LogInformation("confirm-migration-email email token is invalid");
|
||||
return BadRequest("Invalid credentials");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "bad-credentials"));
|
||||
}
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -845,7 +883,7 @@ public class AccountController : BaseApiController
|
|||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences),
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -859,12 +897,12 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return BadRequest("User does not exist");
|
||||
if (user == null) return BadRequest(await _localizationService.Get("en", "no-user"));
|
||||
|
||||
if (string.IsNullOrEmpty(user.Email))
|
||||
return BadRequest(
|
||||
"This user needs to migrate. Have them log out and login to trigger a migration flow");
|
||||
if (user.EmailConfirmed) return BadRequest("User already confirmed");
|
||||
await _localizationService.Translate(user.Id, "user-migration-needed"));
|
||||
if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "user-already-confirmed"));
|
||||
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email);
|
||||
|
@ -885,12 +923,12 @@ public class AccountController : BaseApiController
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue resending invite email");
|
||||
return BadRequest("There was an issue resending invite email");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "generic-invite-email"));
|
||||
}
|
||||
return Ok(emailLink);
|
||||
}
|
||||
|
||||
return Ok("The server is not accessible externally");
|
||||
return Ok(await _localizationService.Translate(user.Id, "not-accessible"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -904,7 +942,7 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
// If there is an admin account already, return
|
||||
var users = await _unitOfWork.UserRepository.GetAdminUsersAsync();
|
||||
if (users.Any()) return BadRequest("Admin already exists");
|
||||
if (users.Any()) return BadRequest(await _localizationService.Get("en", "admin-already-exists"));
|
||||
|
||||
// Check if there is an existing invite
|
||||
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
|
||||
|
@ -912,27 +950,27 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||
return BadRequest($"User is already registered as {invitedUser!.UserName}");
|
||||
return BadRequest(await _localizationService.Get("en", "user-already-registered", invitedUser!.UserName));
|
||||
|
||||
_logger.LogInformation("A user is attempting to login, but hasn't accepted email invite");
|
||||
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
||||
return BadRequest(await _localizationService.Get("en", "user-already-invited"));
|
||||
}
|
||||
|
||||
|
||||
var user = await _userManager.Users
|
||||
.Include(u => u.UserPreferences)
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == dto.Username.ToUpper());
|
||||
if (user == null) return BadRequest("Invalid username");
|
||||
if (user == null) return BadRequest(await _localizationService.Get("en", "invalid-username"));
|
||||
|
||||
var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password);
|
||||
if (!validPassword) return BadRequest("Your credentials are not correct");
|
||||
if (!validPassword) return BadRequest(await _localizationService.Get("en", "bad-credentials"));
|
||||
|
||||
try
|
||||
{
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
|
||||
user.Email = dto.Email;
|
||||
if (!await ConfirmEmailToken(token, user)) return BadRequest("There was a critical error during migration");
|
||||
if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "critical-email-migration"));
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -946,16 +984,16 @@ public class AccountController : BaseApiController
|
|||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
return BadRequest("There was an error setting up your account. Please check the logs");
|
||||
return BadRequest(await _localizationService.Get("en", "critical-email-migration"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task<bool> ConfirmEmailToken(string token, AppUser user)
|
||||
{
|
||||
var result = await _userManager.ConfirmEmailAsync(user, token);
|
||||
if (result.Succeeded) return true;
|
||||
|
||||
|
||||
|
||||
_logger.LogCritical("[Account] Email validation failed");
|
||||
if (!result.Errors.Any()) return false;
|
||||
|
||||
|
@ -965,6 +1003,36 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the OPDS url for this user
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("opds-url")]
|
||||
public async Task<ActionResult<string>> GetOpdsUrl()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
|
||||
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
var origin = HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value;
|
||||
if (!string.IsNullOrEmpty(serverSettings.HostName)) origin = serverSettings.HostName;
|
||||
|
||||
var baseUrl = string.Empty;
|
||||
if (!string.IsNullOrEmpty(serverSettings.BaseUrl) &&
|
||||
!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl))
|
||||
{
|
||||
baseUrl = serverSettings.BaseUrl + "/";
|
||||
if (baseUrl.EndsWith("//"))
|
||||
{
|
||||
baseUrl = baseUrl.Replace("//", "/");
|
||||
}
|
||||
|
||||
if (baseUrl.StartsWith("/"))
|
||||
{
|
||||
baseUrl = baseUrl.Substring(1, baseUrl.Length - 1);
|
||||
}
|
||||
}
|
||||
return Ok(origin + "/" + baseUrl + "api/opds/" + user!.ApiKey);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using API.Data;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -18,13 +19,16 @@ public class BookController : BaseApiController
|
|||
private readonly IBookService _bookService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public BookController(IBookService bookService,
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_bookService = bookService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cacheService = cacheService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -37,20 +41,20 @@ public class BookController : BaseApiController
|
|||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||
{
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
if (dto == null) return BadRequest("Chapter does not exist");
|
||||
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var bookTitle = string.Empty;
|
||||
switch (dto.SeriesFormat)
|
||||
{
|
||||
case MangaFormat.Epub:
|
||||
{
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0];
|
||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
|
||||
bookTitle = book.Title;
|
||||
break;
|
||||
}
|
||||
case MangaFormat.Pdf:
|
||||
{
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0];
|
||||
if (string.IsNullOrEmpty(bookTitle))
|
||||
{
|
||||
// Override with filename
|
||||
|
@ -92,15 +96,16 @@ public class BookController : BaseApiController
|
|||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
if (chapterId <= 0) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist"));
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter is not valid");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist"));
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
|
||||
var key = BookService.CoalesceKeyForAnyFile(book, file);
|
||||
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
|
||||
|
||||
var bookFile = book.Content.AllFiles[key];
|
||||
if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Get("en", "file-missing"));
|
||||
|
||||
var bookFile = book.Content.AllFiles.GetLocalFileRefByKey(key);
|
||||
var content = await bookFile.ReadContentAsBytesAsync();
|
||||
|
||||
var contentType = BookService.GetContentType(bookFile.ContentType);
|
||||
|
@ -117,9 +122,9 @@ public class BookController : BaseApiController
|
|||
[HttpGet("{chapterId}/chapters")]
|
||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter is not valid");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -143,7 +148,7 @@ public class BookController : BaseApiController
|
|||
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
||||
{
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var path = _cacheService.GetCachedFile(chapter);
|
||||
|
||||
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
|
||||
|
@ -154,8 +159,7 @@ public class BookController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,16 +20,19 @@ public class CollectionController : BaseApiController
|
|||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICollectionTagService _collectionService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService)
|
||||
public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_collectionService = collectionService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a list of all collection tags on the server
|
||||
/// Return a list of all collection tags on the server for the logged in user.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
|
@ -87,14 +90,14 @@ public class CollectionController : BaseApiController
|
|||
{
|
||||
try
|
||||
{
|
||||
if (await _collectionService.UpdateTag(updatedTag)) return Ok("Tag updated successfully");
|
||||
if (await _collectionService.UpdateTag(updatedTag)) return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated-successfully"));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
return BadRequest("Something went wrong, please try again");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -111,7 +114,7 @@ public class CollectionController : BaseApiController
|
|||
|
||||
if (await _collectionService.AddTagToSeries(tag, dto.SeriesIds)) return Ok();
|
||||
|
||||
return BadRequest("There was an issue updating series with collection tag");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -126,18 +129,41 @@ public class CollectionController : BaseApiController
|
|||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata);
|
||||
if (tag == null) return BadRequest("Not a valid Tag");
|
||||
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
||||
|
||||
if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove))
|
||||
return Ok("Tag updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated"));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
|
||||
return BadRequest("Something went wrong. Please try again.");
|
||||
/// <summary>
|
||||
/// Removes the collection tag from all Series it was attached to
|
||||
/// </summary>
|
||||
/// <param name="tagId"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteTag(int tagId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId, CollectionTagIncludes.SeriesMetadata);
|
||||
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
||||
|
||||
if (await _collectionService.DeleteTag(tag))
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "collection-deleted"));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,16 @@ public class DeviceController : BaseApiController
|
|||
private readonly IDeviceService _deviceService;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService, IEmailService emailService, IEventHub eventHub)
|
||||
public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService,
|
||||
IEmailService emailService, IEventHub eventHub, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_deviceService = deviceService;
|
||||
_emailService = emailService;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
|
||||
|
@ -36,9 +39,19 @@ public class DeviceController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
var device = await _deviceService.Create(dto, user);
|
||||
try
|
||||
{
|
||||
var device = await _deviceService.Create(dto, user);
|
||||
if (device == null)
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-create"));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (device == null) return BadRequest("There was an error when creating the device");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
@ -50,7 +63,7 @@ public class DeviceController : BaseApiController
|
|||
if (user == null) return Unauthorized();
|
||||
var device = await _deviceService.Update(dto, user);
|
||||
|
||||
if (device == null) return BadRequest("There was an error when updating the device");
|
||||
if (device == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-update"));
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
@ -63,32 +76,33 @@ public class DeviceController : BaseApiController
|
|||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteDevice(int deviceId)
|
||||
{
|
||||
if (deviceId <= 0) return BadRequest("Not a valid deviceId");
|
||||
if (deviceId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "device-doesnt-exist"));
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
if (await _deviceService.Delete(user, deviceId)) return Ok();
|
||||
|
||||
return BadRequest("Could not delete device");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-delete"));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<DeviceDto>>> GetDevices()
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(userId));
|
||||
return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
[HttpPost("send-to")]
|
||||
public async Task<ActionResult> SendToDevice(SendToDeviceDto dto)
|
||||
{
|
||||
if (dto.ChapterIds.Any(i => i < 0)) return BadRequest("ChapterIds must be greater than 0");
|
||||
if (dto.DeviceId < 0) return BadRequest("DeviceId must be greater than 0");
|
||||
if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "ChapterIds"));
|
||||
if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId"));
|
||||
|
||||
if (await _emailService.IsDefaultEmailService())
|
||||
return BadRequest("Send to device cannot be used with Kavita's email service. Please configure your own.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
|
||||
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "started"), userId);
|
||||
var userId = User.GetUserId();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"started"), userId);
|
||||
try
|
||||
{
|
||||
var success = await _deviceService.SendTo(dto.ChapterIds, dto.DeviceId);
|
||||
|
@ -96,14 +110,56 @@ public class DeviceController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "ended"), userId);
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"ended"), userId);
|
||||
}
|
||||
|
||||
return BadRequest("There was an error sending the file to the device");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost("send-series-to")]
|
||||
public async Task<ActionResult> SendSeriesToDevice(SendSeriesToDeviceDto dto)
|
||||
{
|
||||
if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId"));
|
||||
if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId"));
|
||||
|
||||
if (await _emailService.IsDefaultEmailService())
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
|
||||
|
||||
var userId = User.GetUserId();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"started"), userId);
|
||||
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId,
|
||||
SeriesIncludes.Volumes | SeriesIncludes.Chapters);
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
||||
var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList();
|
||||
try
|
||||
{
|
||||
var success = await _deviceService.SendTo(chapterIds, dto.DeviceId);
|
||||
if (success) return Ok();
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"ended"), userId);
|
||||
}
|
||||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -30,11 +30,12 @@ public class DownloadController : BaseApiController
|
|||
private readonly ILogger<DownloadController> _logger;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private const string DefaultContentType = "application/octet-stream";
|
||||
|
||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
|
||||
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService,
|
||||
IAccountService accountService)
|
||||
IAccountService accountService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_archiveService = archiveService;
|
||||
|
@ -44,6 +45,7 @@ public class DownloadController : BaseApiController
|
|||
_logger = logger;
|
||||
_bookmarkService = bookmarkService;
|
||||
_accountService = accountService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -92,9 +94,9 @@ public class DownloadController : BaseApiController
|
|||
[HttpGet("volume")]
|
||||
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
|
||||
if (volume == null) return BadRequest("Volume doesn't exist");
|
||||
if (volume == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "volume-doesnt-exist"));
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
|
@ -117,7 +119,7 @@ public class DownloadController : BaseApiController
|
|||
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
|
||||
{
|
||||
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
|
||||
return PhysicalFile(zipFile, contentType, fileDownloadName, true);
|
||||
return PhysicalFile(zipFile, contentType, Uri.EscapeDataString(fileDownloadName), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -128,10 +130,10 @@ public class DownloadController : BaseApiController
|
|||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Invalid chapter");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId);
|
||||
try
|
||||
|
@ -163,7 +165,7 @@ public class DownloadController : BaseApiController
|
|||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
return PhysicalFile(filePath, DefaultContentType, downloadName, true);
|
||||
return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -178,7 +180,7 @@ public class DownloadController : BaseApiController
|
|||
[HttpGet("series")]
|
||||
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
if (series == null) return BadRequest("Invalid Series");
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
|
@ -200,8 +202,8 @@ public class DownloadController : BaseApiController
|
|||
[HttpPost("bookmarks")]
|
||||
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
|
||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmarks-empty"));
|
||||
|
||||
// We know that all bookmarks will be for one single seriesId
|
||||
var userId = User.GetUserId()!;
|
||||
|
@ -220,7 +222,7 @@ public class DownloadController : BaseApiController
|
|||
MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 1F));
|
||||
|
||||
|
||||
return PhysicalFile(filePath, DefaultContentType, filename, true);
|
||||
return PhysicalFile(filePath, DefaultContentType, System.Web.HttpUtility.UrlEncode(filename), true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ public class FallbackController : Controller
|
|||
_taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
public ActionResult Index()
|
||||
public PhysicalFileResult Index()
|
||||
{
|
||||
return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML");
|
||||
}
|
||||
|
|
59
API/Controllers/FilterController.cs
Normal file
59
API/Controllers/FilterController.cs
Normal file
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// This is responsible for Filter caching
|
||||
/// </summary>
|
||||
public class FilterController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEasyCachingProviderFactory _cacheFactory;
|
||||
|
||||
public FilterController(IUnitOfWork unitOfWork, IEasyCachingProviderFactory cacheFactory)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_cacheFactory = cacheFactory;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<FilterV2Dto?>> GetFilter(string name)
|
||||
{
|
||||
var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Filter);
|
||||
if (string.IsNullOrEmpty(name)) return Ok(null);
|
||||
var filter = await provider.GetAsync<FilterV2Dto>(name);
|
||||
if (filter.HasValue)
|
||||
{
|
||||
filter.Value.Name = name;
|
||||
return Ok(filter.Value);
|
||||
}
|
||||
|
||||
return Ok(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches the filter in the backend and returns a temp string for retrieving.
|
||||
/// </summary>
|
||||
/// <remarks>The cache line lives for only 1 hour</remarks>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("create-temp")]
|
||||
public async Task<ActionResult<string>> CreateTempFilter(FilterV2Dto filterDto)
|
||||
{
|
||||
var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Filter);
|
||||
var name = filterDto.Name;
|
||||
if (string.IsNullOrEmpty(filterDto.Name))
|
||||
{
|
||||
name = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
await provider.SetAsync(name, filterDto, TimeSpan.FromHours(1));
|
||||
return name;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
|
@ -20,12 +21,17 @@ public class ImageController : BaseApiController
|
|||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService)
|
||||
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService,
|
||||
IImageService imageService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
_imageService = imageService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -37,9 +43,10 @@ public class ImageController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "apiKey"})]
|
||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
@ -54,9 +61,10 @@ public class ImageController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId", "apiKey"})]
|
||||
public async Task<ActionResult> GetLibraryCoverImage(int libraryId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
@ -71,9 +79,10 @@ public class ImageController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId", "apiKey"})]
|
||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
@ -88,9 +97,10 @@ public class ImageController : BaseApiController
|
|||
[HttpGet("series-cover")]
|
||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
Response.AddCacheHeader(path);
|
||||
|
@ -107,9 +117,16 @@ public class ImageController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId", "apiKey"})]
|
||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
{
|
||||
var destFile = await GenerateCollectionCoverImage(collectionTagId);
|
||||
if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
|
||||
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)),
|
||||
_directoryService.FileSystem.Path.GetFileName(destFile));
|
||||
}
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
@ -124,14 +141,51 @@ public class ImageController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId", "apiKey"})]
|
||||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
{
|
||||
var destFile = await GenerateReadingListCoverImage(readingListId);
|
||||
if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
|
||||
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile));
|
||||
}
|
||||
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
private async Task<string> GenerateReadingListCoverImage(int readingListId)
|
||||
{
|
||||
var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId);
|
||||
var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory,
|
||||
ImageService.GetReadingListFormat(readingListId));
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
destFile += settings.EncodeMediaAs.GetExtension();
|
||||
|
||||
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
|
||||
ImageService.CreateMergedImage(
|
||||
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
|
||||
settings.CoverImageSize,
|
||||
destFile);
|
||||
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateCollectionCoverImage(int collectionId)
|
||||
{
|
||||
var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId);
|
||||
var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory,
|
||||
ImageService.GetCollectionTagFormat(collectionId));
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
destFile += settings.EncodeMediaAs.GetExtension();
|
||||
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
|
||||
ImageService.CreateMergedImage(
|
||||
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
|
||||
settings.CoverImageSize,
|
||||
destFile);
|
||||
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns image for a given bookmark page
|
||||
/// </summary>
|
||||
|
@ -147,7 +201,7 @@ public class ImageController : BaseApiController
|
|||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
|
||||
if (bookmark == null) return BadRequest("Bookmark does not exist");
|
||||
if (bookmark == null) return BadRequest(await _localizationService.Translate(userId, "bookmark-doesnt-exist"));
|
||||
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
|
@ -157,6 +211,42 @@ public class ImageController : BaseApiController
|
|||
return PhysicalFile(file.FullName, MimeTypeMap.GetMimeType(format), Path.GetFileName(file.FullName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the image associated with a web-link
|
||||
/// </summary>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("web-link")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = new []{"url", "apiKey"})]
|
||||
public async Task<ActionResult> GetWebLinkImage(string url, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(userId, "must-be-defined", "Url"));
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
// Check if the domain exists
|
||||
var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url, encodeFormat));
|
||||
if (!_directoryService.FileSystem.File.Exists(domainFilePath))
|
||||
{
|
||||
// We need to request the favicon and save it
|
||||
try
|
||||
{
|
||||
domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory,
|
||||
await _imageService.DownloadFaviconAsync(url, encodeFormat));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(userId, "generic-favicon"));
|
||||
}
|
||||
}
|
||||
|
||||
var file = new FileInfo(domainFilePath);
|
||||
var format = Path.GetExtension(file.FullName);
|
||||
|
||||
return PhysicalFile(file.FullName, MimeTypeMap.GetMimeType(format), Path.GetFileName(file.FullName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a temp coverupload image
|
||||
/// </summary>
|
||||
|
@ -168,10 +258,11 @@ public class ImageController : BaseApiController
|
|||
public async Task<ActionResult> GetCoverUploadImage(string filename, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
if (filename.Contains("..")) return BadRequest("Invalid Filename");
|
||||
if (filename.Contains("..")) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-filename"));
|
||||
|
||||
var path = Path.Join(_directoryService.TempDirectory, filename);
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "file-doesnt-exist"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
|
@ -12,10 +13,12 @@ using API.Entities;
|
|||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -33,10 +36,14 @@ public class LibraryController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEasyCachingProvider _libraryCacheProvider;
|
||||
private const string CacheKey = "library_";
|
||||
|
||||
public LibraryController(IDirectoryService directoryService,
|
||||
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
|
||||
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher)
|
||||
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher,
|
||||
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService)
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
_logger = logger;
|
||||
|
@ -45,28 +52,41 @@ public class LibraryController : BaseApiController
|
|||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
_localizationService = localizationService;
|
||||
|
||||
_libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Library. Upon library creation, adds new library to all Admin accounts.
|
||||
/// </summary>
|
||||
/// <param name="createLibraryDto"></param>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("create")]
|
||||
public async Task<ActionResult> AddLibrary(CreateLibraryDto createLibraryDto)
|
||||
public async Task<ActionResult> AddLibrary(UpdateLibraryDto dto)
|
||||
{
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(createLibraryDto.Name))
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(dto.Name))
|
||||
{
|
||||
return BadRequest("Library name already exists. Please choose a unique name to the server.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-name-exists"));
|
||||
}
|
||||
|
||||
var library = new Library
|
||||
var library = new LibraryBuilder(dto.Name, dto.Type)
|
||||
.WithFolders(dto.Folders.Select(x => new FolderPath {Path = x}).Distinct().ToList())
|
||||
.WithFolderWatching(dto.FolderWatching)
|
||||
.WithIncludeInDashboard(dto.IncludeInDashboard)
|
||||
.WithIncludeInRecommended(dto.IncludeInRecommended)
|
||||
.WithManageCollections(dto.ManageCollections)
|
||||
.WithManageReadingLists(dto.ManageReadingLists)
|
||||
.WIthAllowScrobbling(dto.AllowScrobbling)
|
||||
.Build();
|
||||
|
||||
// Override Scrobbling for Comic libraries since there are no providers to scrobble to
|
||||
if (library.Type == LibraryType.Comic)
|
||||
{
|
||||
Name = createLibraryDto.Name,
|
||||
Type = createLibraryDto.Type,
|
||||
Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList()
|
||||
};
|
||||
_logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name);
|
||||
library.AllowScrobbling = false;
|
||||
}
|
||||
|
||||
_unitOfWork.LibraryRepository.Add(library);
|
||||
|
||||
|
@ -78,13 +98,14 @@ public class LibraryController : BaseApiController
|
|||
}
|
||||
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue. Please try again.");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
|
||||
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
|
||||
await _libraryWatcher.RestartWatching();
|
||||
_taskScheduler.ScanLibrary(library.Id);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(library.Id, "create"), false);
|
||||
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -106,7 +127,7 @@ public class LibraryController : BaseApiController
|
|||
}));
|
||||
}
|
||||
|
||||
if (!Directory.Exists(path)) return BadRequest("This is not a valid path");
|
||||
if (!Directory.Exists(path)) return Ok(_directoryService.ListDirectory(Path.GetDirectoryName(path)));
|
||||
|
||||
return Ok(_directoryService.ListDirectory(path));
|
||||
}
|
||||
|
@ -118,7 +139,18 @@ public class LibraryController : BaseApiController
|
|||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
|
||||
var username = User.GetUsername();
|
||||
if (string.IsNullOrEmpty(username)) return Unauthorized();
|
||||
|
||||
var cacheKey = CacheKey + username;
|
||||
var result = await _libraryCacheProvider.GetAsync<IEnumerable<LibraryDto>>(cacheKey);
|
||||
if (result.HasValue) return Ok(result.Value);
|
||||
|
||||
var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username);
|
||||
await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24));
|
||||
_logger.LogDebug("Caching libraries for {Key}", cacheKey);
|
||||
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -129,8 +161,8 @@ public class LibraryController : BaseApiController
|
|||
[HttpGet("jump-bar")]
|
||||
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library");
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, User.GetUserId()))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-library-access"));
|
||||
|
||||
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
|
||||
}
|
||||
|
@ -145,9 +177,9 @@ public class LibraryController : BaseApiController
|
|||
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
|
||||
if (user == null) return BadRequest("Could not validate user");
|
||||
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-doesnt-exist"));
|
||||
|
||||
var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
|
||||
var libraryString = string.Join(',', updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
|
||||
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);
|
||||
|
||||
var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
|
@ -165,23 +197,24 @@ public class LibraryController : BaseApiController
|
|||
{
|
||||
library.AppUsers.Add(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges())
|
||||
{
|
||||
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
|
||||
_logger.LogInformation("No changes for update library access");
|
||||
return Ok(_mapper.Map<MemberDto>(user));
|
||||
}
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
|
||||
// Bust cache
|
||||
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
|
||||
return Ok(_mapper.Map<MemberDto>(user));
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -192,9 +225,9 @@ public class LibraryController : BaseApiController
|
|||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan")]
|
||||
public ActionResult Scan(int libraryId, bool force = false)
|
||||
public async Task<ActionResult> Scan(int libraryId, bool force = false)
|
||||
{
|
||||
if (libraryId <= 0) return BadRequest("Invalid libraryId");
|
||||
if (libraryId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "libraryId"));
|
||||
_taskScheduler.ScanLibrary(libraryId, force);
|
||||
return Ok();
|
||||
}
|
||||
|
@ -245,7 +278,7 @@ public class LibraryController : BaseApiController
|
|||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!isAdmin) return BadRequest("API key must belong to an admin");
|
||||
|
||||
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
|
||||
if (dto.FolderPath.Contains("..")) return BadRequest(await _localizationService.Translate(user.Id, "invalid-path"));
|
||||
|
||||
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
|
||||
|
||||
|
@ -278,12 +311,11 @@ public class LibraryController : BaseApiController
|
|||
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
|
||||
{
|
||||
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
|
||||
return BadRequest(
|
||||
"You cannot delete a library while a scan is in progress. Please wait for scan to complete or restart Kavita then try to delete");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "delete-library-while-scan"));
|
||||
}
|
||||
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
if (library == null) return BadRequest("Library no longer exists");
|
||||
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
|
||||
|
||||
// Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library
|
||||
// Aka SeriesRelation has an invalid foreign key
|
||||
|
@ -299,6 +331,8 @@ public class LibraryController : BaseApiController
|
|||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
|
||||
|
||||
if (chapterIds.Any())
|
||||
{
|
||||
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
||||
|
@ -320,7 +354,7 @@ public class LibraryController : BaseApiController
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was a critical error trying to delete the library");
|
||||
_logger.LogError(ex, await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return Ok(false);
|
||||
}
|
||||
|
@ -335,9 +369,8 @@ public class LibraryController : BaseApiController
|
|||
[HttpGet("name-exists")]
|
||||
public async Task<ActionResult<bool>> IsLibraryNameValid(string name)
|
||||
{
|
||||
var trimmed = name.Trim();
|
||||
if (string.IsNullOrEmpty(trimmed)) return Ok(true);
|
||||
return Ok(await _unitOfWork.LibraryRepository.LibraryExists(trimmed));
|
||||
if (string.IsNullOrWhiteSpace(name)) return Ok(true);
|
||||
return Ok(await _unitOfWork.LibraryRepository.LibraryExists(name.Trim()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -351,16 +384,16 @@ public class LibraryController : BaseApiController
|
|||
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto dto)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders);
|
||||
if (library == null) return BadRequest("Library doesn't exist");
|
||||
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
|
||||
|
||||
var newName = dto.Name.Trim();
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName))
|
||||
return BadRequest("Library name already exists");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-name-exists"));
|
||||
|
||||
var originalFolders = library.Folders.Select(x => x.Path).ToList();
|
||||
|
||||
library.Name = newName;
|
||||
library.Folders = dto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||
library.Folders = dto.Folders.Select(s => new FolderPath() {Path = s}).Distinct().ToList();
|
||||
|
||||
var typeUpdate = library.Type != dto.Type;
|
||||
var folderWatchingUpdate = library.FolderWatching != dto.FolderWatching;
|
||||
|
@ -371,11 +404,19 @@ public class LibraryController : BaseApiController
|
|||
library.IncludeInSearch = dto.IncludeInSearch;
|
||||
library.ManageCollections = dto.ManageCollections;
|
||||
library.ManageReadingLists = dto.ManageReadingLists;
|
||||
library.AllowScrobbling = dto.AllowScrobbling;
|
||||
|
||||
// Override Scrobbling for Comic libraries since there are no providers to scrobble to
|
||||
if (library.Type == LibraryType.Comic)
|
||||
{
|
||||
_logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name);
|
||||
library.AllowScrobbling = false;
|
||||
}
|
||||
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library-update"));
|
||||
if (originalFolders.Count != dto.Folders.Count() || typeUpdate)
|
||||
{
|
||||
await _libraryWatcher.RestartWatching();
|
||||
|
@ -389,6 +430,8 @@ public class LibraryController : BaseApiController
|
|||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(library.Id, "update"), false);
|
||||
|
||||
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
|
||||
|
||||
return Ok();
|
||||
|
||||
}
|
||||
|
|
90
API/Controllers/LicenseController.cs
Normal file
90
API/Controllers/LicenseController.cs
Normal file
|
@ -0,0 +1,90 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs.Account;
|
||||
using API.DTOs.License;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class LicenseController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<LicenseController> _logger;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public LicenseController(IUnitOfWork unitOfWork, ILogger<LicenseController> logger,
|
||||
ILicenseService licenseService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_licenseService = licenseService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the user's license is valid or not
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("valid-license")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
|
||||
public async Task<ActionResult<bool>> HasValidLicense(bool forceCheck = false)
|
||||
{
|
||||
return Ok(await _licenseService.HasActiveLicense(forceCheck));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Has any license
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpGet("has-license")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
|
||||
public async Task<ActionResult<bool>> HasLicense()
|
||||
{
|
||||
return Ok(!string.IsNullOrEmpty(
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value));
|
||||
}
|
||||
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpDelete]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
|
||||
public async Task<ActionResult> RemoveLicense()
|
||||
{
|
||||
_logger.LogInformation("Removing license on file for Server");
|
||||
var setting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
|
||||
setting.Value = null;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates server license
|
||||
/// </summary>
|
||||
/// <remarks>Caches the result</remarks>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> UpdateLicense(UpdateLicenseDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
}
|
32
API/Controllers/LocaleController.cs
Normal file
32
API/Controllers/LocaleController.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using API.DTOs.Filtering;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class LocaleController : BaseApiController
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public LocaleController(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<IEnumerable<string>> GetAllLocales()
|
||||
{
|
||||
var languages = _localizationService.GetLocales().Select(c => new CultureInfo(c)).Select(c =>
|
||||
new LanguageDto()
|
||||
{
|
||||
Title = c.DisplayName,
|
||||
IsoCode = c.IetfLanguageTag
|
||||
})
|
||||
.Where(l => !string.IsNullOrEmpty(l.IsoCode))
|
||||
.OrderBy(d => d.Title);
|
||||
return Ok(languages);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ using API.DTOs.Filtering;
|
|||
using API.DTOs.Metadata;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -19,10 +20,12 @@ namespace API.Controllers;
|
|||
public class MetadataController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public MetadataController(IUnitOfWork unitOfWork)
|
||||
public MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -34,17 +37,28 @@ public class MetadataController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
{
|
||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, userId));
|
||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
}
|
||||
|
||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(userId));
|
||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fetches people from the instance by role
|
||||
/// </summary>
|
||||
/// <param name="role">role</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("people-by-role")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"role"})]
|
||||
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(PersonRole? role)
|
||||
{
|
||||
return role.HasValue ?
|
||||
Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role!.Value)) :
|
||||
Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches people from the instance
|
||||
|
@ -55,13 +69,12 @@ public class MetadataController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
{
|
||||
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, userId));
|
||||
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
}
|
||||
return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(userId));
|
||||
return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -73,13 +86,12 @@ public class MetadataController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
{
|
||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, userId));
|
||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
}
|
||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(userId));
|
||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -92,7 +104,7 @@ public class MetadataController : BaseApiController
|
|||
[HttpGet("age-ratings")]
|
||||
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
|
||||
|
@ -115,7 +127,7 @@ public class MetadataController : BaseApiController
|
|||
[HttpGet("publication-status")]
|
||||
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(_unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
|
||||
|
@ -135,19 +147,14 @@ public class MetadataController : BaseApiController
|
|||
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("languages")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
||||
}
|
||||
|
||||
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync());
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("all-languages")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
|
||||
public IEnumerable<LanguageDto> GetAllValidLanguages()
|
||||
|
@ -160,6 +167,7 @@ public class MetadataController : BaseApiController
|
|||
}).Where(l => !string.IsNullOrEmpty(l.IsoCode));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns summary for the chapter
|
||||
/// </summary>
|
||||
|
@ -168,9 +176,9 @@ public class MetadataController : BaseApiController
|
|||
[HttpGet("chapter-summary")]
|
||||
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter does not exist");
|
||||
if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter does not exist");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
return Ok(chapter.Summary);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ using API.Data.Repositories;
|
|||
using API.DTOs;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.OPDS;
|
||||
using API.DTOs.Search;
|
||||
using API.Entities;
|
||||
|
@ -24,6 +25,8 @@ using MimeTypes;
|
|||
|
||||
namespace API.Controllers;
|
||||
|
||||
#nullable enable
|
||||
|
||||
[AllowAnonymous]
|
||||
public class OpdsController : BaseApiController
|
||||
{
|
||||
|
@ -34,6 +37,7 @@ public class OpdsController : BaseApiController
|
|||
private readonly IReaderService _readerService;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
|
||||
private readonly XmlSerializer _xmlSerializer;
|
||||
|
@ -62,13 +66,15 @@ public class OpdsController : BaseApiController
|
|||
SortOptions = null,
|
||||
PublicationStatus = new List<PublicationStatus>()
|
||||
};
|
||||
|
||||
private readonly FilterV2Dto _filterV2Dto = new FilterV2Dto();
|
||||
private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default;
|
||||
private const int PageSize = 20;
|
||||
|
||||
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
||||
IDirectoryService directoryService, ICacheService cacheService,
|
||||
IReaderService readerService, ISeriesService seriesService,
|
||||
IAccountService accountService)
|
||||
IAccountService accountService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_downloadService = downloadService;
|
||||
|
@ -77,6 +83,7 @@ public class OpdsController : BaseApiController
|
|||
_readerService = readerService;
|
||||
_seriesService = seriesService;
|
||||
_accountService = accountService;
|
||||
_localizationService = localizationService;
|
||||
|
||||
_xmlSerializer = new XmlSerializer(typeof(Feed));
|
||||
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
|
||||
|
@ -87,20 +94,21 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> Get(string apiKey)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix);
|
||||
SetFeedId(feed, "root");
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "onDeck",
|
||||
Title = "On Deck",
|
||||
Title = await _localizationService.Translate(userId, "on-deck"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by On Deck"
|
||||
Text = await _localizationService.Translate(userId, "browse-on-deck")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -110,10 +118,10 @@ public class OpdsController : BaseApiController
|
|||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "recentlyAdded",
|
||||
Title = "Recently Added",
|
||||
Title = await _localizationService.Translate(userId, "recently-added"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Recently Added"
|
||||
Text = await _localizationService.Translate(userId, "browse-recently-added")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -123,10 +131,10 @@ public class OpdsController : BaseApiController
|
|||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "readingList",
|
||||
Title = "Reading Lists",
|
||||
Title = await _localizationService.Translate(userId, "reading-lists"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Reading Lists"
|
||||
Text = await _localizationService.Translate(userId, "browse-reading-lists")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -135,11 +143,24 @@ public class OpdsController : BaseApiController
|
|||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "allLibraries",
|
||||
Title = "All Libraries",
|
||||
Id = "wantToRead",
|
||||
Title = await _localizationService.Translate(userId, "want-to-read"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Libraries"
|
||||
Text = await _localizationService.Translate(userId, "browse-want-to-read")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/want-to-read"),
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "allLibraries",
|
||||
Title = await _localizationService.Translate(userId, "libraries"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = await _localizationService.Translate(userId, "browse-libraries")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -149,10 +170,10 @@ public class OpdsController : BaseApiController
|
|||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "allCollections",
|
||||
Title = "All Collections",
|
||||
Title = await _localizationService.Translate(userId, "collections"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Collections"
|
||||
Text = await _localizationService.Translate(userId, "browse-collections")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -180,12 +201,12 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetLibraries(string apiKey)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId);
|
||||
var feed = CreateFeed("All Libraries", $"{prefix}{apiKey}/libraries", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{prefix}{apiKey}/libraries", apiKey, prefix);
|
||||
SetFeedId(feed, "libraries");
|
||||
foreach (var library in libraries)
|
||||
{
|
||||
|
@ -196,6 +217,8 @@ public class OpdsController : BaseApiController
|
|||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/libraries/{library.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/library-cover?libraryId={library.Id}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/library-cover?libraryId={library.Id}&apiKey={apiKey}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -203,14 +226,35 @@ public class OpdsController : BaseApiController
|
|||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/want-to-read")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetWantToRead(string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var wantToReadSeries = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(userId, GetUserParams(pageNumber), _filterV2Dto);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(wantToReadSeries.Select(s => s.Id));
|
||||
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "want-to-read"), $"{apiKey}/want-to-read", apiKey, prefix);
|
||||
SetFeedId(feed, $"want-to-read");
|
||||
AddPagination(feed, wantToReadSeries, $"{prefix}{apiKey}/want-to-read");
|
||||
|
||||
feed.Entries.AddRange(wantToReadSeries.Select(seriesDto =>
|
||||
CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl)));
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/collections")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetCollections(string apiKey)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
@ -219,23 +263,21 @@ public class OpdsController : BaseApiController
|
|||
: (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId));
|
||||
|
||||
|
||||
var feed = CreateFeed("All Collections", $"{prefix}{apiKey}/collections", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{prefix}{apiKey}/collections", apiKey, prefix);
|
||||
SetFeedId(feed, "collections");
|
||||
foreach (var tag in tags)
|
||||
|
||||
feed.Entries.AddRange(tags.Select(tag => new FeedEntry()
|
||||
{
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
Id = tag.Id.ToString(),
|
||||
Title = tag.Title,
|
||||
Summary = tag.Summary,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
Id = tag.Id.ToString(),
|
||||
Title = tag.Title,
|
||||
Summary = tag.Summary,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections/{tag.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={tag.Id}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={tag.Id}&apiKey={apiKey}")
|
||||
}
|
||||
});
|
||||
}
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections/{tag.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionTagId={tag.Id}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionTagId={tag.Id}&apiKey={apiKey}")
|
||||
}
|
||||
}));
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
@ -245,10 +287,10 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
@ -272,7 +314,7 @@ public class OpdsController : BaseApiController
|
|||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, GetUserParams(pageNumber));
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id));
|
||||
|
||||
var feed = CreateFeed(tag.Title + " Collection", $"{prefix}{apiKey}/collections/{collectionId}", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(tag.Title + " Collection", $"{prefix}{apiKey}/collections/{collectionId}", apiKey, prefix);
|
||||
SetFeedId(feed, $"collections-{collectionId}");
|
||||
AddPagination(feed, series, $"{prefix}{apiKey}/collections/{collectionId}");
|
||||
|
||||
|
@ -289,16 +331,16 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetReadingLists(string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId,
|
||||
true, GetUserParams(pageNumber), false);
|
||||
|
||||
|
||||
var feed = CreateFeed("All Reading Lists", $"{prefix}{apiKey}/reading-list", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed("All Reading Lists", $"{prefix}{apiKey}/reading-list", apiKey, prefix);
|
||||
SetFeedId(feed, "reading-list");
|
||||
foreach (var readingListDto in readingLists)
|
||||
{
|
||||
|
@ -310,6 +352,8 @@ public class OpdsController : BaseApiController
|
|||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/reading-list/{readingListDto.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/readinglist-cover?readingListId={readingListDto.Id}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/readinglist-cover?readingListId={readingListDto.Id}&apiKey={apiKey}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -330,10 +374,10 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetReadingListItems(int readingListId, string apiKey)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
||||
var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user!.UserName!, AppUserIncludes.ReadingListsWithItems);
|
||||
|
@ -341,10 +385,10 @@ public class OpdsController : BaseApiController
|
|||
var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId);
|
||||
if (readingList == null)
|
||||
{
|
||||
return BadRequest("Reading list does not exist or you don't have access");
|
||||
return BadRequest(await _localizationService.Translate(userId, "reading-list-restricted"));
|
||||
}
|
||||
|
||||
var feed = CreateFeed(readingList.Title + " Reading List", $"{prefix}{apiKey}/reading-list/{readingListId}", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{prefix}{apiKey}/reading-list/{readingListId}", apiKey, prefix);
|
||||
SetFeedId(feed, $"reading-list-{readingListId}");
|
||||
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).ToList();
|
||||
|
@ -361,29 +405,39 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var library =
|
||||
(await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).SingleOrDefault(l =>
|
||||
l.Id == libraryId);
|
||||
if (library == null)
|
||||
{
|
||||
return BadRequest("User does not have access to this library");
|
||||
return BadRequest(await _localizationService.Translate(userId, "no-library-access"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, GetUserParams(pageNumber), _filterDto);
|
||||
var filter = new FilterV2Dto
|
||||
{
|
||||
Statements = new List<FilterStatementDto>() {
|
||||
new ()
|
||||
{
|
||||
Comparison = FilterComparison.Equal,
|
||||
Field = FilterField.Libraries,
|
||||
Value = libraryId + string.Empty
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(pageNumber), filter);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id));
|
||||
|
||||
var feed = CreateFeed(library.Name, $"{apiKey}/libraries/{libraryId}", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(library.Name, $"{apiKey}/libraries/{libraryId}", apiKey, prefix);
|
||||
SetFeedId(feed, $"library-{library.Name}");
|
||||
AddPagination(feed, series, $"{prefix}{apiKey}/libraries/{libraryId}");
|
||||
|
||||
foreach (var seriesDto in series)
|
||||
{
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl));
|
||||
}
|
||||
feed.Entries.AddRange(series.Select(seriesDto =>
|
||||
CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl)));
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
@ -392,14 +446,14 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = 1)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAdded(0, userId, GetUserParams(pageNumber), _filterDto);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, GetUserParams(pageNumber), _filterV2Dto);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id));
|
||||
|
||||
var feed = CreateFeed("Recently Added", $"{prefix}{apiKey}/recently-added", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "recently-added"), $"{prefix}{apiKey}/recently-added", apiKey, prefix);
|
||||
SetFeedId(feed, "recently-added");
|
||||
AddPagination(feed, recentlyAdded, $"{prefix}{apiKey}/recently-added");
|
||||
|
||||
|
@ -415,19 +469,19 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetOnDeck(string apiKey, [FromQuery] int pageNumber = 1)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
var userId = await GetUser(apiKey);
|
||||
var userParams = GetUserParams(pageNumber);
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(pagedList.Select(s => s.Id));
|
||||
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
var feed = CreateFeed("On Deck", $"{prefix}{apiKey}/on-deck", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "on-deck"), $"{prefix}{apiKey}/on-deck", apiKey, prefix);
|
||||
SetFeedId(feed, "on-deck");
|
||||
AddPagination(feed, pagedList, $"{prefix}{apiKey}/on-deck");
|
||||
|
||||
|
@ -443,26 +497,26 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> SearchSeries(string apiKey, [FromQuery] string query)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return BadRequest("You must pass a query parameter");
|
||||
return BadRequest(await _localizationService.Translate(userId, "query-required"));
|
||||
}
|
||||
query = query.Replace(@"%", string.Empty);
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) 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 feed = CreateFeed(query, $"{prefix}{apiKey}/series?query=" + query, apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(query, $"{prefix}{apiKey}/series?query=" + query, apiKey, prefix);
|
||||
SetFeedId(feed, "search-series");
|
||||
foreach (var seriesDto in series.Series)
|
||||
{
|
||||
|
@ -515,13 +569,14 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSearchDescriptor(string apiKey)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (_, prefix) = await GetPrefix();
|
||||
var feed = new OpenSearchDescription()
|
||||
{
|
||||
ShortName = "Search",
|
||||
Description = "Search for Series, Collections, or Reading Lists",
|
||||
ShortName = await _localizationService.Translate(userId, "search"),
|
||||
Description = await _localizationService.Translate(userId, "search-description"),
|
||||
Url = new SearchLink()
|
||||
{
|
||||
Type = FeedLinkType.AtomAcquisition,
|
||||
|
@ -539,13 +594,13 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSeries(string apiKey, int seriesId)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix, baseUrl);
|
||||
var feed = CreateFeed(series!.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix);
|
||||
SetFeedId(feed, $"series-{series.Id}");
|
||||
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}&apiKey={apiKey}"));
|
||||
|
||||
|
@ -561,7 +616,7 @@ public class OpdsController : BaseApiController
|
|||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -573,7 +628,7 @@ public class OpdsController : BaseApiController
|
|||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(storylineChapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -583,7 +638,7 @@ public class OpdsController : BaseApiController
|
|||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(special.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -594,26 +649,26 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetVolume(string apiKey, int seriesId, int volumeId)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
||||
var chapters =
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number),
|
||||
_chapterSortComparer);
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{SeriesService.FormatChapterName(libraryType)}s");
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s");
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id);
|
||||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -624,23 +679,23 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetChapter(string apiKey, int seriesId, int volumeId, int chapterId)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter doesn't exist");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist"));
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{SeriesService.FormatChapterName(libraryType)}-{chapterId}-files");
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix);
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{_seriesService.FormatChapterName(userId, libraryType)}-{chapterId}-files");
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
|
@ -658,8 +713,9 @@ public class OpdsController : BaseApiController
|
|||
[HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}")]
|
||||
public async Task<ActionResult> DownloadFile(string apiKey, int seriesId, int volumeId, int chapterId, string filename)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
|
||||
if (!await _accountService.HasDownloadPermission(user))
|
||||
{
|
||||
|
@ -723,8 +779,10 @@ public class OpdsController : BaseApiController
|
|||
return new FeedEntry()
|
||||
{
|
||||
Id = seriesDto.Id.ToString(),
|
||||
Title = $"{seriesDto.Name} ({seriesDto.Format})",
|
||||
Summary = seriesDto.Summary,
|
||||
Title = $"{seriesDto.Name}",
|
||||
Summary = $"Format: {seriesDto.Format}" + (string.IsNullOrWhiteSpace(metadata.Summary)
|
||||
? string.Empty
|
||||
: $" Summary: {metadata.Summary}"),
|
||||
Authors = metadata.Writers.Select(p => new FeedAuthor()
|
||||
{
|
||||
Name = p.Name,
|
||||
|
@ -749,7 +807,8 @@ public class OpdsController : BaseApiController
|
|||
return new FeedEntry()
|
||||
{
|
||||
Id = searchResultDto.SeriesId.ToString(),
|
||||
Title = $"{searchResultDto.Name} ({searchResultDto.Format})",
|
||||
Title = $"{searchResultDto.Name}",
|
||||
Summary = $"Format: {searchResultDto.Format}",
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/series/{searchResultDto.SeriesId}"),
|
||||
|
@ -778,7 +837,7 @@ public class OpdsController : BaseApiController
|
|||
};
|
||||
}
|
||||
|
||||
private async Task<FeedEntry> CreateChapterWithFile(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl)
|
||||
private async Task<FeedEntry> CreateChapterWithFile(int userId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl)
|
||||
{
|
||||
var fileSize =
|
||||
mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) :
|
||||
|
@ -794,7 +853,8 @@ public class OpdsController : BaseApiController
|
|||
|
||||
if (volume!.Chapters.Count == 1)
|
||||
{
|
||||
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType);
|
||||
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
||||
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType, volumeLabel);
|
||||
if (volume.Name != "0")
|
||||
{
|
||||
title += $" - {volume.Name}";
|
||||
|
@ -802,11 +862,11 @@ public class OpdsController : BaseApiController
|
|||
}
|
||||
else if (volume.Number != 0)
|
||||
{
|
||||
title = $"{series.Name} - Volume {volume.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
|
||||
title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
title = $"{series.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
|
||||
title = $"{series.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
|
||||
}
|
||||
|
||||
// Chunky requires a file at the end. Our API ignores this
|
||||
|
@ -854,14 +914,16 @@ public class OpdsController : BaseApiController
|
|||
[HttpGet("{apiKey}/image")]
|
||||
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber)
|
||||
{
|
||||
if (pageNumber < 0) return BadRequest("Page cannot be less than 0");
|
||||
var userId = await GetUser(apiKey);
|
||||
if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page"));
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "cache-file-find"));
|
||||
|
||||
try
|
||||
{
|
||||
var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {pageNumber}");
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
|
||||
return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber));
|
||||
|
||||
var content = await _directoryService.ReadFileAsync(path);
|
||||
var format = Path.GetExtension(path);
|
||||
|
@ -892,8 +954,9 @@ public class OpdsController : BaseApiController
|
|||
[ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Client, NoStore = false)]
|
||||
public async Task<ActionResult> GetFavicon(string apiKey)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico");
|
||||
if (files.Length == 0) return BadRequest("Cannot find icon");
|
||||
if (files.Length == 0) return BadRequest(await _localizationService.Translate(userId, "favicon-doesnt-exist"));
|
||||
var path = files[0];
|
||||
var content = await _directoryService.ReadFileAsync(path);
|
||||
var format = Path.GetExtension(path);
|
||||
|
@ -916,7 +979,7 @@ public class OpdsController : BaseApiController
|
|||
{
|
||||
/* Do nothing */
|
||||
}
|
||||
throw new KavitaException("User does not exist");
|
||||
throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist"));
|
||||
}
|
||||
|
||||
private async Task<FeedLink> CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey, string prefix)
|
||||
|
@ -948,7 +1011,7 @@ public class OpdsController : BaseApiController
|
|||
};
|
||||
}
|
||||
|
||||
private static Feed CreateFeed(string title, string href, string apiKey, string prefix, string baseUrl)
|
||||
private static Feed CreateFeed(string title, string href, string apiKey, string prefix)
|
||||
{
|
||||
var link = CreateLink(FeedLinkRelation.Self, string.IsNullOrEmpty(href) ?
|
||||
FeedLinkType.AtomNavigation :
|
||||
|
|
77
API/Controllers/RatingController.cs
Normal file
77
API/Controllers/RatingController.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Extensions;
|
||||
using API.Services.Plus;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for providing external ratings for Series
|
||||
/// </summary>
|
||||
public class RatingController : BaseApiController
|
||||
{
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly IRatingService _ratingService;
|
||||
private readonly ILogger<RatingController> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEasyCachingProvider _cacheProvider;
|
||||
public const string CacheKey = "rating_";
|
||||
|
||||
public RatingController(ILicenseService licenseService, IRatingService ratingService,
|
||||
ILogger<RatingController> logger, IEasyCachingProviderFactory cachingProviderFactory, IUnitOfWork unitOfWork)
|
||||
{
|
||||
_licenseService = licenseService;
|
||||
_ratingService = ratingService;
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
|
||||
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the external ratings for a given series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})]
|
||||
public async Task<ActionResult<IEnumerable<RatingDto>>> GetRating(int seriesId)
|
||||
{
|
||||
|
||||
if (!await _licenseService.HasActiveLicense())
|
||||
{
|
||||
return Ok(Enumerable.Empty<RatingDto>());
|
||||
}
|
||||
|
||||
var cacheKey = CacheKey + seriesId;
|
||||
var results = await _cacheProvider.GetAsync<IEnumerable<RatingDto>>(cacheKey);
|
||||
if (results.HasValue)
|
||||
{
|
||||
return Ok(results.Value);
|
||||
}
|
||||
|
||||
var ratings = await _ratingService.GetRatings(seriesId);
|
||||
await _cacheProvider.SetAsync(cacheKey, ratings, TimeSpan.FromHours(24));
|
||||
_logger.LogDebug("Caching external rating for {Key}", cacheKey);
|
||||
return Ok(ratings);
|
||||
}
|
||||
|
||||
[HttpGet("overall")]
|
||||
public async Task<ActionResult<RatingDto>> GetOverallRating(int seriesId)
|
||||
{
|
||||
return Ok(new RatingDto()
|
||||
{
|
||||
Provider = ScrobbleProvider.Kavita,
|
||||
AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()),
|
||||
FavoriteCount = 0
|
||||
});
|
||||
}
|
||||
}
|
|
@ -8,13 +8,16 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -35,12 +38,16 @@ public class ReaderController : BaseApiController
|
|||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(ICacheService cacheService,
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
||||
IReaderService readerService, IBookmarkService bookmarkService,
|
||||
IAccountService accountService, IEventHub eventHub)
|
||||
IAccountService accountService, IEventHub eventHub,
|
||||
IScrobblingService scrobblingService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_unitOfWork = unitOfWork;
|
||||
|
@ -49,6 +56,8 @@ public class ReaderController : BaseApiController
|
|||
_bookmarkService = bookmarkService;
|
||||
_accountService = accountService;
|
||||
_eventHub = eventHub;
|
||||
_scrobblingService = scrobblingService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -62,20 +71,20 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("There was an issue finding pdf file for reading");
|
||||
if (chapter == null) return NoContent();
|
||||
|
||||
// Validate the user has access to the PDF
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapter.Id,
|
||||
await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()));
|
||||
if (series == null) return BadRequest("Invalid Access");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-access"));
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
var path = _cacheService.GetCachedFile(chapter);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"Pdf doesn't exist when it should.");
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "pdf-doesnt-exist"));
|
||||
|
||||
return PhysicalFile(path, "application/pdf", Path.GetFileName(path), true);
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(Path.GetExtension(path)), Path.GetFileName(path), true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -99,14 +108,16 @@ public class ReaderController : BaseApiController
|
|||
public async Task<ActionResult> GetImage(int chapterId, int page, string apiKey, bool extractPdf = false)
|
||||
{
|
||||
if (page < 0) page = 0;
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
|
||||
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
|
||||
if (chapter == null) return NoContent();
|
||||
|
||||
try
|
||||
{
|
||||
var path = _cacheService.GetCachedPagePath(chapter.Id, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache.");
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
|
||||
return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page));
|
||||
var format = Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true);
|
||||
|
@ -118,14 +129,22 @@ public class ReaderController : BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a thumbnail for the given page number
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="pageNum"></param>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("thumbnail")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "pageNum", "apiKey"})]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetThumbnail(int chapterId, int pageNum, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var chapter = await _cacheService.Ensure(chapterId, true);
|
||||
if (chapter == null) return BadRequest("There was an issue extracting images from chapter");
|
||||
if (chapter == null) return NoContent();
|
||||
var images = _cacheService.GetCachedPages(chapterId);
|
||||
|
||||
var path = await _readerService.GetThumbnail(chapter, pageNum, images);
|
||||
|
@ -148,7 +167,7 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
if (page < 0) page = 0;
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
if (userId == 0) return Unauthorized();
|
||||
|
||||
var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId);
|
||||
if (page > totalPages)
|
||||
|
@ -159,7 +178,7 @@ public class ReaderController : BaseApiController
|
|||
try
|
||||
{
|
||||
var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page));
|
||||
var format = Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path));
|
||||
|
@ -185,7 +204,7 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
if (chapterId <= 0) return ArraySegment<FileDimensionDto>.Empty;
|
||||
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
if (chapter == null) return NoContent();
|
||||
return Ok(_cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId)));
|
||||
}
|
||||
|
||||
|
@ -203,10 +222,10 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore
|
||||
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
if (chapter == null) return NoContent();
|
||||
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
if (dto == null) return BadRequest("Please perform a scan on this series or library and try again");
|
||||
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "perform-scan"));
|
||||
var mangaFile = chapter.Files.First();
|
||||
|
||||
var info = new ChapterInfoDto()
|
||||
|
@ -245,7 +264,8 @@ public class ReaderController : BaseApiController
|
|||
}
|
||||
else
|
||||
{
|
||||
info.Subtitle = "Volume " + info.VolumeNumber;
|
||||
//info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber);
|
||||
info.Subtitle = $"Volume {info.VolumeNumber}";
|
||||
if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter))
|
||||
{
|
||||
info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) +
|
||||
|
@ -298,10 +318,19 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
|
||||
try
|
||||
{
|
||||
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId));
|
||||
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -318,8 +347,9 @@ public class ReaderController : BaseApiController
|
|||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -339,10 +369,11 @@ public class ReaderController : BaseApiController
|
|||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -357,17 +388,23 @@ public class ReaderController : BaseApiController
|
|||
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||
try
|
||||
{
|
||||
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId,
|
||||
markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages)));
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId));
|
||||
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
|
@ -391,13 +428,12 @@ public class ReaderController : BaseApiController
|
|||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList());
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId));
|
||||
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id));
|
||||
return Ok();
|
||||
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -422,10 +458,11 @@ public class ReaderController : BaseApiController
|
|||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -446,12 +483,14 @@ public class ReaderController : BaseApiController
|
|||
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
foreach (var sId in dto.SeriesIds)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId));
|
||||
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(sId, user.Id));
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -474,10 +513,14 @@ public class ReaderController : BaseApiController
|
|||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
foreach (var sId in dto.SeriesIds)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId));
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -505,11 +548,14 @@ public class ReaderController : BaseApiController
|
|||
/// <param name="progressDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("progress")]
|
||||
public async Task<ActionResult> BookmarkProgress(ProgressDto progressDto)
|
||||
public async Task<ActionResult> SaveProgress(ProgressDto progressDto)
|
||||
{
|
||||
if (await _readerService.SaveReadingProgress(progressDto, User.GetUserId())) return Ok(true);
|
||||
var userId = User.GetUserId();
|
||||
if (!await _readerService.SaveReadingProgress(progressDto, userId))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -520,9 +566,7 @@ public class ReaderController : BaseApiController
|
|||
[HttpGet("continue-point")]
|
||||
public async Task<ActionResult<ChapterDto>> GetContinuePoint(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
|
||||
return Ok(await _readerService.GetContinuePoint(seriesId, userId));
|
||||
return Ok(await _readerService.GetContinuePoint(seriesId, User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -533,8 +577,7 @@ public class ReaderController : BaseApiController
|
|||
[HttpGet("has-progress")]
|
||||
public async Task<ActionResult<bool>> HasProgress(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId));
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -545,10 +588,7 @@ public class ReaderController : BaseApiController
|
|||
[HttpGet("chapter-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(User.GetUserId(), chapterId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -557,13 +597,9 @@ public class ReaderController : BaseApiController
|
|||
/// <param name="filterDto">Only supports SeriesNameQuery</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("all-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterDto filterDto)
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterV2Dto filterDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
|
||||
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto));
|
||||
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(User.GetUserId(), filterDto));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -576,7 +612,7 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||
if (user.Bookmarks == null) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -603,7 +639,7 @@ public class ReaderController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not clear bookmarks");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-clear-bookmarks"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -616,7 +652,7 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||
if (user.Bookmarks == null) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -640,7 +676,7 @@ public class ReaderController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not clear bookmarks");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-clear-bookmarks"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -651,10 +687,7 @@ public class ReaderController : BaseApiController
|
|||
[HttpGet("volume-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId));
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(User.GetUserId(), volumeId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -665,11 +698,7 @@ public class ReaderController : BaseApiController
|
|||
[HttpGet("series-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId));
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(User.GetUserId(), seriesId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -686,15 +715,16 @@ public class ReaderController : BaseApiController
|
|||
if (user == null) return new UnauthorizedResult();
|
||||
|
||||
if (!await _accountService.HasBookmarkPermission(user))
|
||||
return BadRequest("You do not have permission to bookmark");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
|
||||
|
||||
var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId);
|
||||
if (chapter == null) return BadRequest("Could not find cached image. Reload and try again.");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find"));
|
||||
|
||||
bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page);
|
||||
var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page);
|
||||
|
||||
if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest("Could not save bookmark");
|
||||
if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||
return Ok();
|
||||
|
@ -713,10 +743,10 @@ public class ReaderController : BaseApiController
|
|||
if (user.Bookmarks.IsNullOrEmpty()) return Ok();
|
||||
|
||||
if (!await _accountService.HasBookmarkPermission(user))
|
||||
return BadRequest("You do not have permission to unbookmark");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
|
||||
|
||||
if (!await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto))
|
||||
return BadRequest("Could not remove bookmark");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save"));
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
|
@ -735,8 +765,7 @@ public class ReaderController : BaseApiController
|
|||
[HttpGet("next-chapter")]
|
||||
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
||||
return await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, User.GetUserId());
|
||||
}
|
||||
|
||||
|
||||
|
@ -754,8 +783,7 @@ public class ReaderController : BaseApiController
|
|||
[HttpGet("prev-chapter")]
|
||||
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
||||
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, User.GetUserId());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -768,7 +796,7 @@ public class ReaderController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId"})]
|
||||
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var userId = User.GetUserId();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
|
||||
// Get all sum of all chapters with progress that is complete then subtract from series. Multiply by modifiers
|
||||
|
@ -788,4 +816,60 @@ public class ReaderController : BaseApiController
|
|||
return _readerService.GetTimeEstimate(0, pagesLeft, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the user's personal table of contents for the given chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("ptoc")]
|
||||
public ActionResult<IEnumerable<PersonalToCDto>> GetPersonalToC(int chapterId)
|
||||
{
|
||||
return Ok(_unitOfWork.UserTableOfContentRepository.GetPersonalToC(User.GetUserId(), chapterId));
|
||||
}
|
||||
|
||||
[HttpDelete("ptoc")]
|
||||
public async Task<ActionResult> DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (string.IsNullOrWhiteSpace(title)) return BadRequest(await _localizationService.Translate(userId, "name-required"));
|
||||
if (pageNum < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number"));
|
||||
var toc = await _unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title);
|
||||
if (toc == null) return Ok();
|
||||
_unitOfWork.UserTableOfContentRepository.Remove(toc);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new personal table of content entry for a given chapter
|
||||
/// </summary>
|
||||
/// <remarks>The title and page number must be unique to that book</remarks>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("create-ptoc")]
|
||||
public async Task<ActionResult> CreatePersonalToC(CreatePersonalToCDto dto)
|
||||
{
|
||||
// Validate there isn't already an existing page title combo?
|
||||
var userId = User.GetUserId();
|
||||
if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest(await _localizationService.Translate(userId, "name-required"));
|
||||
if (dto.PageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number"));
|
||||
if (await _unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber,
|
||||
dto.Title.Trim()))
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(userId, "duplicate-bookmark"));
|
||||
}
|
||||
|
||||
_unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent()
|
||||
{
|
||||
Title = dto.Title.Trim(),
|
||||
ChapterId = dto.ChapterId,
|
||||
PageNumber = dto.PageNumber,
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
BookScrollId = dto.BookScrollId,
|
||||
AppUserId = userId
|
||||
});
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,14 +20,15 @@ namespace API.Controllers;
|
|||
public class ReadingListController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_readingListService = readingListService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -38,8 +39,7 @@ public class ReadingListController : BaseApiController
|
|||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId));
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -53,8 +53,7 @@ public class ReadingListController : BaseApiController
|
|||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams,
|
||||
bool includePromoted = true, bool sortByLastModified = false)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(User.GetUserId(), includePromoted,
|
||||
userParams, sortByLastModified);
|
||||
Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages);
|
||||
|
||||
|
@ -69,10 +68,8 @@ public class ReadingListController : BaseApiController
|
|||
[HttpGet("lists-for-series")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true);
|
||||
|
||||
return Ok(items);
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(User.GetUserId(),
|
||||
seriesId, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -101,13 +98,13 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok("Updated");
|
||||
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
|
||||
|
||||
return BadRequest("Couldn't update position");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-position"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -121,15 +118,15 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
if (await _readingListService.DeleteReadingListItem(dto))
|
||||
{
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
|
||||
return BadRequest("Couldn't delete item");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-item-delete"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -143,15 +140,15 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
if (await _readingListService.RemoveFullyReadItems(readingListId, user))
|
||||
{
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
|
||||
return BadRequest("Could not remove read items");
|
||||
return BadRequest("Couldn't delete item(s)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -165,12 +162,13 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
if (await _readingListService.DeleteReadingList(readingListId, user)) return Ok("List was deleted");
|
||||
if (await _readingListService.DeleteReadingList(readingListId, user))
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-deleted"));
|
||||
|
||||
return BadRequest("There was an issue deleting reading list");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-delete"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -190,7 +188,7 @@ public class ReadingListController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title));
|
||||
|
@ -205,12 +203,12 @@ public class ReadingListController : BaseApiController
|
|||
public async Task<ActionResult> UpdateList(UpdateReadingListDto dto)
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
var user = await _readingListService.UserHasReadingListAccess(readingList.Id, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
try
|
||||
|
@ -219,10 +217,10 @@ public class ReadingListController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -236,11 +234,11 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
var chapterIdsForSeries =
|
||||
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
|
||||
|
||||
|
@ -255,7 +253,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -263,7 +261,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -278,10 +276,10 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||
foreach (var chapterId in dto.ChapterIds)
|
||||
|
@ -300,7 +298,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -308,7 +306,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -322,10 +320,10 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
|
||||
|
||||
|
@ -343,7 +341,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -351,7 +349,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
[HttpPost("update-by-volume")]
|
||||
|
@ -360,10 +358,10 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
var chapterIdsForVolume =
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
|
||||
|
@ -379,7 +377,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -387,7 +385,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
[HttpPost("update-by-chapter")]
|
||||
|
@ -396,10 +394,10 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
|
||||
|
@ -412,7 +410,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -420,7 +418,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -448,7 +446,7 @@ public class ReadingListController : BaseApiController
|
|||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
|
||||
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
|
||||
if (readingListItem == null) return BadRequest("Id does not exist");
|
||||
if (readingListItem == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var index = items.IndexOf(readingListItem) + 1;
|
||||
if (items.Count > index)
|
||||
{
|
||||
|
@ -469,7 +467,7 @@ public class ReadingListController : BaseApiController
|
|||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
|
||||
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
|
||||
if (readingListItem == null) return BadRequest("Id does not exist");
|
||||
if (readingListItem == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var index = items.IndexOf(readingListItem) - 1;
|
||||
if (0 <= index)
|
||||
{
|
||||
|
|
|
@ -1,19 +1,72 @@
|
|||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class RecommendedController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IRecommendationService _recommendationService;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEasyCachingProvider _cacheProvider;
|
||||
public const string CacheKey = "recommendation_";
|
||||
|
||||
public RecommendedController(IUnitOfWork unitOfWork)
|
||||
public RecommendedController(IUnitOfWork unitOfWork, IRecommendationService recommendationService,
|
||||
ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_recommendationService = recommendationService;
|
||||
_licenseService = licenseService;
|
||||
_localizationService = localizationService;
|
||||
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For Kavita+ users, this will return recommendations on the server.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("recommendations")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})]
|
||||
public async Task<ActionResult<RecommendationDto>> GetRecommendations(int seriesId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (!await _licenseService.HasActiveLicense())
|
||||
{
|
||||
return Ok(new RecommendationDto());
|
||||
}
|
||||
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId))
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-restricted"));
|
||||
}
|
||||
|
||||
var cacheKey = $"{CacheKey}-{seriesId}-{userId}";
|
||||
var results = await _cacheProvider.GetAsync<RecommendationDto>(cacheKey);
|
||||
if (results.HasValue)
|
||||
{
|
||||
return Ok(results.Value);
|
||||
}
|
||||
|
||||
var ret = await _recommendationService.GetRecommendationsForSeries(userId, seriesId);
|
||||
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(10));
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
|
||||
|
@ -26,7 +79,7 @@ public class RecommendedController : BaseApiController
|
|||
[HttpGet("quick-reads")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
userParams ??= new UserParams();
|
||||
userParams ??= UserParams.Default;
|
||||
var series = await _unitOfWork.SeriesRepository.GetQuickReads(User.GetUserId(), libraryId, userParams);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
@ -42,7 +95,7 @@ public class RecommendedController : BaseApiController
|
|||
[HttpGet("quick-catchup-reads")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
userParams ??= new UserParams();
|
||||
userParams ??= UserParams.Default;
|
||||
var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(User.GetUserId(), libraryId, userParams);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
@ -58,8 +111,8 @@ public class RecommendedController : BaseApiController
|
|||
[HttpGet("highly-rated")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var userId = User.GetUserId()!;
|
||||
userParams ??= new UserParams();
|
||||
var userId = User.GetUserId();
|
||||
userParams ??= UserParams.Default;
|
||||
var series = await _unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams);
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
@ -78,7 +131,7 @@ public class RecommendedController : BaseApiController
|
|||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
userParams ??= new UserParams();
|
||||
userParams ??= UserParams.Default;
|
||||
var series = await _unitOfWork.SeriesRepository.GetMoreIn(userId, libraryId, genreId, userParams);
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
|
@ -95,7 +148,7 @@ public class RecommendedController : BaseApiController
|
|||
[HttpGet("rediscover")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
userParams ??= new UserParams();
|
||||
userParams ??= UserParams.Default;
|
||||
var series = await _unitOfWork.SeriesRepository.GetRediscover(User.GetUserId(), libraryId, userParams);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
|
157
API/Controllers/ReviewController.cs
Normal file
157
API/Controllers/ReviewController.cs
Normal file
|
@ -0,0 +1,157 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using AutoMapper;
|
||||
using EasyCaching.Core;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class ReviewController : BaseApiController
|
||||
{
|
||||
private readonly ILogger<ReviewController> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IReviewService _reviewService;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly IEasyCachingProvider _cacheProvider;
|
||||
public const string CacheKey = "review_";
|
||||
|
||||
public ReviewController(ILogger<ReviewController> logger, IUnitOfWork unitOfWork, ILicenseService licenseService,
|
||||
IMapper mapper, IReviewService reviewService, IScrobblingService scrobblingService,
|
||||
IEasyCachingProviderFactory cachingProviderFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_licenseService = licenseService;
|
||||
_mapper = mapper;
|
||||
_reviewService = reviewService;
|
||||
_scrobblingService = scrobblingService;
|
||||
|
||||
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fetches reviews from the server for a given series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
[HttpGet]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})]
|
||||
public async Task<ActionResult<IEnumerable<UserReviewDto>>> GetReviews(int seriesId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var userRatings = (await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, userId))
|
||||
.Where(r => !string.IsNullOrEmpty(r.Body) && !string.IsNullOrEmpty(r.Tagline))
|
||||
.ToList();
|
||||
if (!await _licenseService.HasActiveLicense())
|
||||
{
|
||||
return Ok(userRatings);
|
||||
}
|
||||
|
||||
var cacheKey = CacheKey + seriesId;
|
||||
IList<UserReviewDto> externalReviews;
|
||||
|
||||
var result = await _cacheProvider.GetAsync<IEnumerable<UserReviewDto>>(cacheKey);
|
||||
if (result.HasValue)
|
||||
{
|
||||
externalReviews = result.Value.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
var reviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList();
|
||||
externalReviews = SelectSpectrumOfReviews(reviews);
|
||||
|
||||
await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10));
|
||||
_logger.LogDebug("Caching external reviews for {Key}", cacheKey);
|
||||
}
|
||||
|
||||
|
||||
// Fetch external reviews and splice them in
|
||||
userRatings.AddRange(externalReviews);
|
||||
|
||||
|
||||
return Ok(userRatings);
|
||||
}
|
||||
|
||||
private static IList<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
|
||||
{
|
||||
IList<UserReviewDto> externalReviews;
|
||||
var totalReviews = reviews.Count;
|
||||
|
||||
if (totalReviews > 10)
|
||||
{
|
||||
//var stepSize = Math.Max(totalReviews / 10, 1); // Calculate step size, ensuring it's at least 1
|
||||
var stepSize = Math.Max((totalReviews - 4) / 8, 1);
|
||||
|
||||
var selectedReviews = new List<UserReviewDto>()
|
||||
{
|
||||
reviews[0],
|
||||
reviews[1],
|
||||
};
|
||||
for (var i = 2; i < totalReviews - 2; i += stepSize)
|
||||
{
|
||||
selectedReviews.Add(reviews[i]);
|
||||
|
||||
if (selectedReviews.Count >= 8)
|
||||
break;
|
||||
}
|
||||
|
||||
selectedReviews.Add(reviews[totalReviews - 2]);
|
||||
selectedReviews.Add(reviews[totalReviews - 1]);
|
||||
|
||||
externalReviews = selectedReviews;
|
||||
}
|
||||
else
|
||||
{
|
||||
externalReviews = reviews;
|
||||
}
|
||||
|
||||
return externalReviews;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the review for a given series
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<UserReviewDto>> UpdateReview(UpdateUserReviewDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var ratingBuilder = new RatingBuilder(user.Ratings.FirstOrDefault(r => r.SeriesId == dto.SeriesId));
|
||||
|
||||
var rating = ratingBuilder
|
||||
.WithBody(dto.Body)
|
||||
.WithSeriesId(dto.SeriesId)
|
||||
.WithTagline(dto.Tagline)
|
||||
.Build();
|
||||
|
||||
if (rating.Id == 0)
|
||||
{
|
||||
user.Ratings.Add(rating);
|
||||
}
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
|
||||
BackgroundJob.Enqueue(() =>
|
||||
_scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, dto.Tagline, dto.Body));
|
||||
return Ok(_mapper.Map<UserReviewDto>(rating));
|
||||
}
|
||||
}
|
210
API/Controllers/ScrobblingController.cs
Normal file
210
API/Controllers/ScrobblingController.cs
Normal file
|
@ -0,0 +1,210 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Account;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.Entities.Scrobble;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
|
||||
public class ScrobblingController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILogger<ScrobblingController> _logger;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ScrobblingController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService,
|
||||
ILogger<ScrobblingController> logger, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_scrobblingService = scrobblingService;
|
||||
_logger = logger;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[HttpGet("anilist-token")]
|
||||
public async Task<ActionResult> GetAniListToken()
|
||||
{
|
||||
// Validate the license
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
return Ok(user.AniListAccessToken);
|
||||
}
|
||||
|
||||
[HttpPost("update-anilist-token")]
|
||||
public async Task<ActionResult> UpdateAniListToken(AniListUpdateDto dto)
|
||||
{
|
||||
// Validate the license
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var isNewToken = string.IsNullOrEmpty(user.AniListAccessToken);
|
||||
user.AniListAccessToken = dto.Token;
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (isNewToken)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(user.Id));
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("token-expired")]
|
||||
public async Task<ActionResult<bool>> HasTokenExpired(ScrobbleProvider provider)
|
||||
{
|
||||
return Ok(await _scrobblingService.HasTokenExpired(User.GetUserId(), provider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all scrobbling errors for the instance
|
||||
/// </summary>
|
||||
/// <remarks>Requires admin</remarks>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("scrobble-errors")]
|
||||
public async Task<ActionResult<IEnumerable<ScrobbleErrorDto>>> GetScrobbleErrors()
|
||||
{
|
||||
return Ok(await _unitOfWork.ScrobbleRepository.GetScrobbleErrors());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the scrobbling errors table
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("clear-errors")]
|
||||
public async Task<ActionResult> ClearScrobbleErrors()
|
||||
{
|
||||
await _unitOfWork.ScrobbleRepository.ClearScrobbleErrors();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the scrobbling history for the user
|
||||
/// </summary>
|
||||
/// <remarks>User must have a valid license</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpPost("scrobble-events")]
|
||||
public async Task<ActionResult<PagedList<ScrobbleEventDto>>> GetScrobblingEvents([FromQuery] UserParams pagination, [FromBody] ScrobbleEventFilter filter)
|
||||
{
|
||||
pagination ??= UserParams.Default;
|
||||
var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(User.GetUserId(), filter, pagination);
|
||||
Response.AddPaginationHeader(events.CurrentPage, events.PageSize, events.TotalCount, events.TotalPages);
|
||||
return Ok(events);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all scrobble holds for the current user
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("holds")]
|
||||
public async Task<ActionResult<IEnumerable<ScrobbleHoldDto>>> GetScrobbleHolds()
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetHolds(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If there is an active hold on the series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("has-hold")]
|
||||
public async Task<ActionResult<bool>> HasHold(int seriesId)
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.HasHoldOnSeries(User.GetUserId(), seriesId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the library the series is in allow scrobbling?
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("library-allows-scrobbling")]
|
||||
public async Task<ActionResult<bool>> LibraryAllowsScrobbling(int seriesId)
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllowsScrobblingBySeriesId(seriesId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hold against the Series for user's scrobbling
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("add-hold")]
|
||||
public async Task<ActionResult> AddHold(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId))
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
var seriesHold = new ScrobbleHoldBuilder().WithSeriesId(seriesId).Build();
|
||||
user.ScrobbleHolds.Add(seriesHold);
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
try
|
||||
{
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException ex)
|
||||
{
|
||||
foreach (var entry in ex.Entries)
|
||||
{
|
||||
// Reload the entity from the database
|
||||
await entry.ReloadAsync();
|
||||
}
|
||||
|
||||
// Retry the update
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle other exceptions or log the error
|
||||
_logger.LogError(ex, "An error occurred while adding the hold");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hold against the Series for user's scrobbling
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete("remove-hold")]
|
||||
public async Task<ActionResult> RemoveHold(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.ScrobbleHolds = user.ScrobbleHolds.Where(h => h.SeriesId != seriesId).ToList();
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ using API.Data.Repositories;
|
|||
using API.DTOs;
|
||||
using API.DTOs.Search;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
@ -15,10 +16,12 @@ namespace API.Controllers;
|
|||
public class SearchController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public SearchController(IUnitOfWork unitOfWork)
|
||||
public SearchController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -30,8 +33,7 @@ public class SearchController : BaseApiController
|
|||
[HttpGet("series-for-mangafile")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -43,8 +45,7 @@ public class SearchController : BaseApiController
|
|||
[HttpGet("series-for-chapter")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, User.GetUserId()));
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
|
@ -55,7 +56,7 @@ public class SearchController : BaseApiController
|
|||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList();
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted"));
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
|
@ -13,9 +15,12 @@ using API.Entities.Enums;
|
|||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using EasyCaching.Core;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -27,28 +32,74 @@ public class SeriesController : BaseApiController
|
|||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEasyCachingProvider _ratingCacheProvider;
|
||||
private readonly IEasyCachingProvider _reviewCacheProvider;
|
||||
private readonly IEasyCachingProvider _recommendationCacheProvider;
|
||||
|
||||
|
||||
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, ISeriesService seriesService)
|
||||
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork,
|
||||
ISeriesService seriesService, ILicenseService licenseService,
|
||||
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_seriesService = seriesService;
|
||||
_licenseService = licenseService;
|
||||
_localizationService = localizationService;
|
||||
|
||||
_ratingCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
|
||||
_reviewCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews);
|
||||
_recommendationCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets series with the applied Filter
|
||||
/// </summary>
|
||||
/// <remarks>This is considered v1 and no longer used by Kavita, but will be supported for sometime. See series/v2</remarks>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
[Obsolete("use v2")]
|
||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var userId = User.GetUserId();
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets series with the applied Filter
|
||||
/// </summary>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("v2")]
|
||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibraryV2([FromQuery] UserParams userParams, [FromBody] FilterV2Dto filterDto)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto);
|
||||
|
||||
//TODO: We might want something like libraryId as source so that I don't have to muck with the groups
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series for library");
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
|
@ -59,21 +110,13 @@ public class SeriesController : BaseApiController
|
|||
/// </summary>
|
||||
/// <param name="seriesId">Series Id to fetch details for</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="KavitaException">Throws an exception if the series Id does exist</exception>
|
||||
/// <exception cref="NoContent">Throws an exception if the series Id does exist</exception>
|
||||
[HttpGet("{seriesId:int}")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
try
|
||||
{
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue fetching {SeriesId}", seriesId);
|
||||
throw new KavitaException("This series does not exist");
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, User.GetUserId());
|
||||
if (series == null) return NoContent();
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
|
@ -95,7 +138,7 @@ public class SeriesController : BaseApiController
|
|||
|
||||
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();
|
||||
|
||||
return BadRequest("There was an issue deleting the series requested");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-delete"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -106,21 +149,22 @@ public class SeriesController : BaseApiController
|
|||
[HttpGet("volumes")]
|
||||
public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId));
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, User.GetUserId()));
|
||||
}
|
||||
|
||||
[HttpGet("volume")]
|
||||
public async Task<ActionResult<VolumeDto?>> GetVolume(int volumeId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId));
|
||||
var vol = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, User.GetUserId());
|
||||
if (vol == null) return NoContent();
|
||||
return Ok(vol);
|
||||
}
|
||||
|
||||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult<ChapterDto>> GetChapter(int chapterId)
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
|
||||
if (chapter == null) return NoContent();
|
||||
return Ok(await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter));
|
||||
}
|
||||
|
||||
|
@ -131,32 +175,32 @@ public class SeriesController : BaseApiController
|
|||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update the user rating for the given series
|
||||
/// </summary>
|
||||
/// <param name="updateSeriesRatingDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-rating")]
|
||||
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
|
||||
if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto)) return BadRequest("There was a critical error.");
|
||||
if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Series
|
||||
/// </summary>
|
||||
/// <param name="updateSeries"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
||||
{
|
||||
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
|
||||
if (series == null)
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
||||
|
||||
if (series == null) return BadRequest("Series does not exist");
|
||||
|
||||
var seriesExists =
|
||||
await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name.Trim(), series.LibraryId,
|
||||
series.Format);
|
||||
if (series.Name != updateSeries.Name && seriesExists)
|
||||
{
|
||||
return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library.");
|
||||
}
|
||||
|
||||
series.Name = updateSeries.Name.Trim();
|
||||
series.NormalizedName = series.Name.ToNormalized();
|
||||
if (!string.IsNullOrEmpty(updateSeries.SortName?.Trim()))
|
||||
{
|
||||
|
@ -166,7 +210,6 @@ public class SeriesController : BaseApiController
|
|||
series.LocalizedName = updateSeries.LocalizedName?.Trim();
|
||||
series.NormalizedLocalizedName = series.LocalizedName?.ToNormalized();
|
||||
|
||||
series.NameLocked = updateSeries.NameLocked;
|
||||
series.SortNameLocked = updateSeries.SortNameLocked;
|
||||
series.LocalizedNameLocked = updateSeries.LocalizedNameLocked;
|
||||
|
||||
|
@ -192,19 +235,27 @@ public class SeriesController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("There was an error with updating the series");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-update"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all recently added series. Obsolete, use recently-added-v2
|
||||
/// </summary>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = "Instant")]
|
||||
[HttpPost("recently-added")]
|
||||
[Obsolete("use recently-added-v2")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var userId = User.GetUserId();
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
|
@ -213,23 +264,82 @@ public class SeriesController : BaseApiController
|
|||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all recently added series
|
||||
/// </summary>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = "Instant")]
|
||||
[HttpPost("recently-added-v2")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAddedV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns series that were recently updated, like adding or removing a chapter
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = "Instant")]
|
||||
[HttpPost("recently-updated-series")]
|
||||
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(User.GetUserId(), 20));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all series for the library
|
||||
/// </summary>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("all-v2")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all series for the library. Obsolete, use all-v2
|
||||
/// </summary>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("all")]
|
||||
[Obsolete("User all-v2")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var userId = User.GetUserId();
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
|
@ -241,16 +351,15 @@ public class SeriesController : BaseApiController
|
|||
/// <summary>
|
||||
/// Fetches series that are on deck aka have progress on them.
|
||||
/// </summary>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="libraryId">Default of 0 meaning all libraries</param>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = "Instant")]
|
||||
[HttpPost("on-deck")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck([FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
|
||||
var userId = User.GetUserId();
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, null);
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
|
||||
|
||||
|
@ -259,6 +368,19 @@ public class SeriesController : BaseApiController
|
|||
return Ok(pagedList);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes a series from displaying on deck until the next read event on that series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("remove-from-on-deck")]
|
||||
public async Task<ActionResult> RemoveFromOnDeck([FromQuery] int seriesId)
|
||||
{
|
||||
await _unitOfWork.SeriesRepository.RemoveFromOnDeck(seriesId, User.GetUserId());
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a Cover Image Generation task
|
||||
/// </summary>
|
||||
|
@ -281,7 +403,7 @@ public class SeriesController : BaseApiController
|
|||
[HttpPost("scan")]
|
||||
public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto)
|
||||
{
|
||||
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
|
||||
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -318,12 +440,24 @@ public class SeriesController : BaseApiController
|
|||
[HttpPost("metadata")]
|
||||
public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
|
||||
{
|
||||
if (await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto))
|
||||
if (!await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "update-metadata-fail"));
|
||||
|
||||
if (await _licenseService.HasActiveLicense())
|
||||
{
|
||||
return Ok("Successfully updated");
|
||||
_logger.LogDebug("Clearing cache as series weblinks may have changed");
|
||||
await _reviewCacheProvider.RemoveAsync(ReviewController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId);
|
||||
await _ratingCacheProvider.RemoveAsync(RatingController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId);
|
||||
|
||||
var allUsers = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(s => s.Id);
|
||||
foreach (var userId in allUsers)
|
||||
{
|
||||
await _recommendationCacheProvider.RemoveAsync(RecommendedController.CacheKey + $"{updateSeriesMetadataDto.SeriesMetadata.SeriesId}-{userId}");
|
||||
}
|
||||
}
|
||||
|
||||
return BadRequest("Could not update metadata");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "series-updated"));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -335,12 +469,12 @@ public class SeriesController : BaseApiController
|
|||
[HttpGet("series-by-collection")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var userId = User.GetUserId();
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series for collection");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series-collection"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
|
@ -357,9 +491,8 @@ public class SeriesController : BaseApiController
|
|||
[HttpPost("series-by-ids")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
|
||||
{
|
||||
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
|
||||
if (dto.SeriesIds == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -370,10 +503,11 @@ public class SeriesController : BaseApiController
|
|||
/// <remarks>This is cached for an hour</remarks>
|
||||
[ResponseCache(CacheProfileName = "Month", VaryByQueryKeys = new [] {"ageRating"})]
|
||||
[HttpGet("age-rating")]
|
||||
public ActionResult<string> GetAgeRating(int ageRating)
|
||||
public async Task<ActionResult<string>> GetAgeRating(int ageRating)
|
||||
{
|
||||
var val = (AgeRating) ageRating;
|
||||
if (val == AgeRating.NotApplicable) return "No Restriction";
|
||||
if (val == AgeRating.NotApplicable)
|
||||
return await _localizationService.Translate(User.GetUserId(), "age-restriction-not-applicable");
|
||||
|
||||
return Ok(val.ToDescription());
|
||||
}
|
||||
|
@ -388,8 +522,14 @@ public class SeriesController : BaseApiController
|
|||
[HttpGet("series-detail")]
|
||||
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
try
|
||||
{
|
||||
return await _seriesService.GetSeriesDetail(seriesId, User.GetUserId());
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -403,9 +543,7 @@ public class SeriesController : BaseApiController
|
|||
[HttpGet("related")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation)
|
||||
{
|
||||
// Send back a custom DTO with each type or maybe sorted in some way
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(User.GetUserId(), seriesId, relation));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -416,8 +554,7 @@ public class SeriesController : BaseApiController
|
|||
[HttpGet("all-related")]
|
||||
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _seriesService.GetRelatedSeries(userId, seriesId));
|
||||
return Ok(await _seriesService.GetRelatedSeries(User.GetUserId(), seriesId));
|
||||
}
|
||||
|
||||
|
||||
|
@ -435,7 +572,9 @@ public class SeriesController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("There was an issue updating relationships");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-relationship"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -3,19 +3,26 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs.Jobs;
|
||||
using API.DTOs.MediaErrors;
|
||||
using API.DTOs.Stats;
|
||||
using API.DTOs.Update;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using EasyCaching.Core;
|
||||
using Hangfire;
|
||||
using Hangfire.Storage;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MimeTypes;
|
||||
using TaskScheduler = API.Services.TaskScheduler;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
@ -23,34 +30,37 @@ namespace API.Controllers;
|
|||
[Authorize(Policy = "RequireAdminRole")]
|
||||
public class ServerController : BaseApiController
|
||||
{
|
||||
private readonly IHostApplicationLifetime _applicationLifetime;
|
||||
private readonly ILogger<ServerController> _logger;
|
||||
private readonly IBackupService _backupService;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||
private readonly IStatsService _statsService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IScannerService _scannerService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEasyCachingProviderFactory _cachingProviderFactory;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
|
||||
public ServerController(ILogger<ServerController> logger,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||
ICleanupService cleanupService, IBookmarkService bookmarkService, IScannerService scannerService, IAccountService accountService,
|
||||
ITaskScheduler taskScheduler)
|
||||
ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService,
|
||||
ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_logger = logger;
|
||||
_backupService = backupService;
|
||||
_archiveService = archiveService;
|
||||
_versionUpdaterService = versionUpdaterService;
|
||||
_statsService = statsService;
|
||||
_cleanupService = cleanupService;
|
||||
_bookmarkService = bookmarkService;
|
||||
_scannerService = scannerService;
|
||||
_accountService = accountService;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cachingProviderFactory = cachingProviderFactory;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -96,12 +106,12 @@ public class ServerController : BaseApiController
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("analyze-files")]
|
||||
public ActionResult AnalyzeFiles()
|
||||
public async Task<ActionResult> AnalyzeFiles()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is performing file analysis from admin dashboard", User.GetUsername());
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(ScannerService.Name, "AnalyzeFiles",
|
||||
Array.Empty<object>(), TaskScheduler.DefaultQueue, true))
|
||||
return Ok("Job already running");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "job-already-running"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _scannerService.AnalyzeFiles());
|
||||
return Ok();
|
||||
|
@ -118,28 +128,32 @@ public class ServerController : BaseApiController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the scheduling of the convert bookmarks job. Only one job will run at a time.
|
||||
/// Returns non-sensitive information about the current system
|
||||
/// </summary>
|
||||
/// <remarks>This is just for the UI and is extremely lightweight</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpPost("convert-bookmarks")]
|
||||
public ActionResult ScheduleConvertBookmarks()
|
||||
[HttpGet("server-info-slim")]
|
||||
public async Task<ActionResult<ServerInfoDto>> GetSlimVersion()
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true)) return Ok();
|
||||
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllBookmarkToWebP());
|
||||
return Ok();
|
||||
return Ok(await _statsService.GetServerInfoSlim());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the scheduling of the convert covers job. Only one job will run at a time.
|
||||
/// Triggers the scheduling of the convert media job. This will convert all media to the target encoding (except for PNG). Only one job will run at a time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("convert-covers")]
|
||||
public ActionResult ScheduleConvertCovers()
|
||||
[HttpPost("convert-media")]
|
||||
public async Task<ActionResult> ScheduleConvertCovers()
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true)) return Ok();
|
||||
BackgroundJob.Enqueue(() => _taskScheduler.CovertAllCoversToWebP());
|
||||
var encoding = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
if (encoding == EncodeFormat.PNG)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "encode-as-warning"));
|
||||
}
|
||||
|
||||
_taskScheduler.CovertAllCoversToEncoding();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -148,17 +162,18 @@ public class ServerController : BaseApiController
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("logs")]
|
||||
public ActionResult GetLogs()
|
||||
public async Task<ActionResult> GetLogs()
|
||||
{
|
||||
var files = _backupService.GetLogFiles();
|
||||
try
|
||||
{
|
||||
var zipPath = _archiveService.CreateZipForDownload(files, "logs");
|
||||
return PhysicalFile(zipPath, "application/zip", Path.GetFileName(zipPath), true);
|
||||
return PhysicalFile(zipPath, MimeTypeMap.GetMimeType(Path.GetExtension(zipPath)),
|
||||
System.Web.HttpUtility.UrlEncode(Path.GetFileName(zipPath)), true);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,6 +186,7 @@ public class ServerController : BaseApiController
|
|||
return Ok(await _versionUpdaterService.CheckForUpdate());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Pull the Changelog for Kavita from Github and display
|
||||
/// </summary>
|
||||
|
@ -206,12 +222,53 @@ public class ServerController : BaseApiController
|
|||
Id = dto.Id,
|
||||
Title = dto.Id.Replace('-', ' '),
|
||||
Cron = dto.Cron,
|
||||
CreatedAt = dto.CreatedAt,
|
||||
LastExecution = dto.LastExecution,
|
||||
LastExecutionUtc = dto.LastExecution.HasValue ? new DateTime(dto.LastExecution.Value.Ticks, DateTimeKind.Utc) : null
|
||||
});
|
||||
|
||||
return Ok(recurringJobs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of issues found during scanning or reading in which files may have corruption or bad metadata (structural metadata)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpGet("media-errors")]
|
||||
public ActionResult<PagedList<MediaErrorDto>> GetMediaErrors()
|
||||
{
|
||||
return Ok(_unitOfWork.MediaErrorRepository.GetAllErrorDtosAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all media errors
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("clear-media-alerts")]
|
||||
public async Task<ActionResult> ClearMediaErrors()
|
||||
{
|
||||
await _unitOfWork.MediaErrorRepository.DeleteAll();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Bust Kavita+ Cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("bust-review-and-rec-cache")]
|
||||
public async Task<ActionResult> BustReviewAndRecCache()
|
||||
{
|
||||
_logger.LogInformation("Busting Kavita+ Cache");
|
||||
var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews);
|
||||
await provider.FlushAsync();
|
||||
provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations);
|
||||
await provider.FlushAsync();
|
||||
provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
|
||||
await provider.FlushAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ using API.Services.Tasks.Scanner;
|
|||
using AutoMapper;
|
||||
using Flurl.Http;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Kavita.Common.Extensions;
|
||||
using Kavita.Common.Helpers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -32,9 +33,11 @@ public class SettingsController : BaseApiController
|
|||
private readonly IMapper _mapper;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
|
||||
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher)
|
||||
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
|
@ -43,6 +46,7 @@ public class SettingsController : BaseApiController
|
|||
_mapper = mapper;
|
||||
_emailService = emailService;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[HttpGet("base-url")]
|
||||
|
@ -181,21 +185,55 @@ public class SettingsController : BaseApiController
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.OnDeckProgressDays && updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.OnDeckUpdateDays && updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.CoverImageSize && updateSettingsDto.CoverImageSize + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.CoverImageSize + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskScan;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
|
||||
{
|
||||
if (OsInfo.IsDocker) continue;
|
||||
setting.Value = updateSettingsDto.Port + string.Empty;
|
||||
// Port is managed in appSetting.json
|
||||
Configuration.Port = updateSettingsDto.Port;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.CacheSize && updateSettingsDto.CacheSize + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.CacheSize + string.Empty;
|
||||
// CacheSize is managed in appSetting.json
|
||||
Configuration.CacheSize = updateSettingsDto.CacheSize;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value)
|
||||
{
|
||||
if (OsInfo.IsDocker) continue;
|
||||
// Validate IP addresses
|
||||
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(','))
|
||||
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!IPAddress.TryParse(ipAddress.Trim(), out _)) {
|
||||
return BadRequest($"IP Address '{ipAddress}' is invalid");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid", ipAddress));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,10 +245,10 @@ public class SettingsController : BaseApiController
|
|||
|
||||
if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
|
||||
{
|
||||
var path = !updateSettingsDto.BaseUrl.StartsWith("/")
|
||||
var path = !updateSettingsDto.BaseUrl.StartsWith('/')
|
||||
? $"/{updateSettingsDto.BaseUrl}"
|
||||
: updateSettingsDto.BaseUrl;
|
||||
path = !path.EndsWith("/")
|
||||
path = !path.EndsWith('/')
|
||||
? $"{path}/"
|
||||
: path;
|
||||
setting.Value = path;
|
||||
|
@ -231,22 +269,16 @@ public class SettingsController : BaseApiController
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.ConvertBookmarkToWebP && updateSettingsDto.ConvertBookmarkToWebP + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.EncodeMediaAs && updateSettingsDto.EncodeMediaAs + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.ConvertBookmarkToWebP + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.ConvertCoverToWebP && updateSettingsDto.ConvertCoverToWebP + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.ConvertCoverToWebP + string.Empty;
|
||||
setting.Value = updateSettingsDto.EncodeMediaAs + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = (updateSettingsDto.HostName + string.Empty).Trim();
|
||||
if (setting.Value.EndsWith("/")) setting.Value = setting.Value.Substring(0, setting.Value.Length - 1);
|
||||
if (setting.Value.EndsWith('/')) setting.Value = setting.Value.Substring(0, setting.Value.Length - 1);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
|
@ -256,7 +288,7 @@ public class SettingsController : BaseApiController
|
|||
// Validate new directory can be used
|
||||
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
|
||||
{
|
||||
return BadRequest("Bookmark Directory does not have correct permissions for Kavita to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions"));
|
||||
}
|
||||
|
||||
originalBookmarkDirectory = setting.Value;
|
||||
|
@ -285,7 +317,7 @@ public class SettingsController : BaseApiController
|
|||
{
|
||||
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
||||
{
|
||||
return BadRequest("Total Backups must be between 1 and 30");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-backups"));
|
||||
}
|
||||
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
@ -295,7 +327,7 @@ public class SettingsController : BaseApiController
|
|||
{
|
||||
if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1)
|
||||
{
|
||||
return BadRequest("Total Logs must be between 1 and 30");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-logs"));
|
||||
}
|
||||
setting.Value = updateSettingsDto.TotalLogs + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
@ -343,7 +375,7 @@ public class SettingsController : BaseApiController
|
|||
{
|
||||
_logger.LogError(ex, "There was an exception when updating server settings");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -19,12 +19,15 @@ public class StatsController : BaseApiController
|
|||
private readonly IStatisticService _statService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public StatsController(IStatisticService statService, IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
|
||||
public StatsController(IStatisticService statService, IUnitOfWork unitOfWork,
|
||||
UserManager<AppUser> userManager, ILocalizationService localizationService)
|
||||
{
|
||||
_statService = statService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_userManager = userManager;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[HttpGet("user/{userId}/read")]
|
||||
|
@ -33,7 +36,7 @@ public class StatsController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user!.Id != userId && !await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole))
|
||||
return Unauthorized("You are not authorized to view another user's statistics");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "stats-permission-denied"));
|
||||
|
||||
return Ok(await _statService.GetUserReadStatistics(userId, new List<int>()));
|
||||
}
|
||||
|
@ -122,14 +125,21 @@ public class StatsController : BaseApiController
|
|||
}
|
||||
|
||||
[HttpGet("day-breakdown")]
|
||||
[Authorize("RequireAdminRole")]
|
||||
[ResponseCache(CacheProfileName = "Statistics")]
|
||||
public ActionResult<IEnumerable<StatCount<DayOfWeek>>> GetDayBreakdown()
|
||||
public async Task<ActionResult<IEnumerable<StatCount<DayOfWeek>>>> GetDayBreakdown(int userId = 0)
|
||||
{
|
||||
return Ok(_statService.GetDayBreakdown());
|
||||
if (userId == 0)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!isAdmin) return BadRequest();
|
||||
}
|
||||
|
||||
return Ok(_statService.GetDayBreakdown(userId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("user/reading-history")]
|
||||
[ResponseCache(CacheProfileName = "Statistics")]
|
||||
public async Task<ActionResult<IEnumerable<ReadHistoryEvent>>> GetReadingHistory(int userId)
|
||||
|
|
|
@ -16,11 +16,14 @@ public class TachiyomiController : BaseApiController
|
|||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITachiyomiService _tachiyomiService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService)
|
||||
public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tachiyomiService = tachiyomiService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -31,7 +34,7 @@ public class TachiyomiController : BaseApiController
|
|||
[HttpGet("latest-chapter")]
|
||||
public async Task<ActionResult<ChapterDto>> GetLatestChapter(int seriesId)
|
||||
{
|
||||
if (seriesId < 1) return BadRequest("seriesId must be greater than 0");
|
||||
if (seriesId < 1) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId"));
|
||||
return Ok(await _tachiyomiService.GetLatestChapter(seriesId, User.GetUserId()));
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Theme;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using Kavita.Common;
|
||||
|
@ -15,12 +16,15 @@ public class ThemeController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler)
|
||||
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_themeService = themeService;
|
||||
_taskScheduler = taskScheduler;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[ResponseCache(CacheProfileName = "10Minute")]
|
||||
|
@ -43,7 +47,15 @@ public class ThemeController : BaseApiController
|
|||
[HttpPost("update-default")]
|
||||
public async Task<ActionResult> UpdateDefault(UpdateDefaultThemeDto dto)
|
||||
{
|
||||
await _themeService.UpdateDefault(dto.ThemeId);
|
||||
try
|
||||
{
|
||||
await _themeService.UpdateDefault(dto.ThemeId);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "theme-doesnt-exist"));
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -61,7 +73,7 @@ public class ThemeController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Get("en", ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs.Uploads;
|
||||
using API.Extensions;
|
||||
|
@ -24,10 +25,12 @@ public class UploadController : BaseApiController
|
|||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
|
||||
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub, IReadingListService readingListService)
|
||||
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub, IReadingListService readingListService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_imageService = imageService;
|
||||
|
@ -36,6 +39,7 @@ public class UploadController : BaseApiController
|
|||
_directoryService = directoryService;
|
||||
_eventHub = eventHub;
|
||||
_readingListService = readingListService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -56,9 +60,9 @@ public class UploadController : BaseApiController
|
|||
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
|
||||
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
return BadRequest($"Could not download file");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
|
||||
|
||||
if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");
|
||||
if (!await _imageService.IsImage(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
|
||||
|
||||
return $"coverupload_{dateString}.{format}";
|
||||
}
|
||||
|
@ -66,10 +70,10 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
// Unauthorized
|
||||
if (ex.StatusCode == 401)
|
||||
return BadRequest("The server requires authentication to load the url externally");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
|
||||
}
|
||||
|
||||
return BadRequest("Unable to download image, please use another url or upload by file");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -78,7 +82,7 @@ public class UploadController : BaseApiController
|
|||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
|
||||
[HttpPost("series")]
|
||||
public async Task<ActionResult> UploadSeriesCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
|
@ -86,13 +90,13 @@ public class UploadController : BaseApiController
|
|||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||
if (series == null) return BadRequest("Invalid Series");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
@ -117,7 +121,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Series");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-series-save"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -126,7 +130,7 @@ public class UploadController : BaseApiController
|
|||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
|
||||
[HttpPost("collection")]
|
||||
public async Task<ActionResult> UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
|
@ -134,13 +138,13 @@ public class UploadController : BaseApiController
|
|||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
|
||||
if (tag == null) return BadRequest("Invalid Tag id");
|
||||
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
@ -165,7 +169,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Collection Tag");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-collection-save"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -174,7 +178,7 @@ public class UploadController : BaseApiController
|
|||
/// <remarks>This is the only API that can be called by non-admins, but the authenticated user must have a readinglist permission</remarks>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
|
||||
[HttpPost("reading-list")]
|
||||
public async Task<ActionResult> UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
|
@ -182,16 +186,16 @@ public class UploadController : BaseApiController
|
|||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
if (_readingListService.UserHasReadingListAccess(uploadFileDto.Id, User.GetUsername()) == null)
|
||||
return Unauthorized("You do not have access");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "access-denied"));
|
||||
|
||||
try
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
|
||||
if (readingList == null) return BadRequest("Reading list is not valid");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
@ -216,20 +220,20 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Reading List");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save"));
|
||||
}
|
||||
|
||||
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0)
|
||||
{
|
||||
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
if (thumbnailSize > 0)
|
||||
{
|
||||
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
||||
filename, convertToWebP, thumbnailSize);
|
||||
filename, encodeFormat, thumbnailSize);
|
||||
}
|
||||
|
||||
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
||||
filename, convertToWebP);
|
||||
filename, encodeFormat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -238,7 +242,7 @@ public class UploadController : BaseApiController
|
|||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
|
||||
[HttpPost("chapter")]
|
||||
public async Task<ActionResult> UploadChapterCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
|
@ -246,13 +250,13 @@ public class UploadController : BaseApiController
|
|||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
if (chapter == null) return BadRequest("Invalid Chapter");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
@ -285,7 +289,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Chapter");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-chapter-save"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -294,7 +298,7 @@ public class UploadController : BaseApiController
|
|||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
|
||||
[HttpPost("library")]
|
||||
public async Task<ActionResult> UploadLibraryCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
|
@ -319,7 +323,9 @@ public class UploadController : BaseApiController
|
|||
|
||||
try
|
||||
{
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", ImageService.LibraryThumbnailWidth);
|
||||
var filePath = await CreateThumbnail(uploadFileDto,
|
||||
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}",
|
||||
ImageService.LibraryThumbnailWidth);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
|
@ -342,7 +348,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Library");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-library-save"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -357,7 +363,7 @@ public class UploadController : BaseApiController
|
|||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
if (chapter == null) return BadRequest("Chapter no longer exists");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var originalFile = chapter.CoverImage;
|
||||
chapter.CoverImage = string.Empty;
|
||||
chapter.CoverImageLocked = false;
|
||||
|
@ -382,7 +388,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to resetting cover lock for Chapter");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -18,12 +19,15 @@ public class UsersController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub)
|
||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
|
@ -33,9 +37,12 @@ public class UsersController : BaseApiController
|
|||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
|
||||
_unitOfWork.UserRepository.Delete(user);
|
||||
|
||||
//(TODO: After updating a role or removing a user, delete their token)
|
||||
// await _userManager.RemoveAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName);
|
||||
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
|
||||
return BadRequest("Could not delete the user.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-delete"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -61,16 +68,15 @@ public class UsersController : BaseApiController
|
|||
[HttpGet("has-reading-progress")]
|
||||
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
if (library == null) return BadRequest("Library does not exist");
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
||||
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, User.GetUserId()));
|
||||
}
|
||||
|
||||
[HttpGet("has-library-access")]
|
||||
public async Task<ActionResult<bool>> HasLibraryAccess(int libraryId)
|
||||
public ActionResult<bool> HasLibraryAccess(int libraryId)
|
||||
{
|
||||
var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername());
|
||||
var libs = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername());
|
||||
return Ok(libs.Any(x => x.Id == libraryId));
|
||||
}
|
||||
|
||||
|
@ -109,16 +115,18 @@ public class UsersController : BaseApiController
|
|||
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
||||
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
|
||||
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
|
||||
if (_localizationService.GetLocales().Contains(preferencesDto.Locale))
|
||||
{
|
||||
existingPreferences.Locale = preferencesDto.Locale;
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
return Ok(preferencesDto);
|
||||
}
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-pref"));
|
||||
|
||||
return BadRequest("There was an issue saving preferences.");
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
return Ok(preferencesDto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.WantToRead;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
@ -18,10 +23,34 @@ namespace API.Controllers;
|
|||
public class WantToReadController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public WantToReadController(IUnitOfWork unitOfWork)
|
||||
public WantToReadController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_scrobblingService = scrobblingService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return all Series that are in the current logged in user's Want to Read list, filtered (deprecated, use v2)
|
||||
/// </summary>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
[Obsolete("use v2 instead")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetWantToRead([FromQuery] UserParams userParams, FilterDto filterDto)
|
||||
{
|
||||
userParams ??= new UserParams();
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(User.GetUserId(), userParams, filterDto);
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(User.GetUserId(), pagedList);
|
||||
|
||||
return Ok(pagedList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -30,11 +59,11 @@ public class WantToReadController : BaseApiController
|
|||
/// <param name="userParams"></param>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetWantToRead([FromQuery] UserParams userParams, FilterDto filterDto)
|
||||
[HttpPost("v2")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetWantToReadV2([FromQuery] UserParams userParams, FilterV2Dto filterDto)
|
||||
{
|
||||
userParams ??= new UserParams();
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(User.GetUserId(), userParams, filterDto);
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(User.GetUserId(), userParams, filterDto);
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(User.GetUserId(), pagedList);
|
||||
|
@ -72,9 +101,16 @@ public class WantToReadController : BaseApiController
|
|||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
foreach (var sId in dto.SeriesIds)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, true));
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("There was an issue updating Read List");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-update"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -92,8 +128,16 @@ public class WantToReadController : BaseApiController
|
|||
user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList();
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
foreach (var sId in dto.SeriesIds)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, false));
|
||||
}
|
||||
|
||||
return BadRequest("There was an issue updating Read List");
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-update"));
|
||||
}
|
||||
}
|
||||
|
|
6
API/DTOs/Account/AniListUpdateDto.cs
Normal file
6
API/DTOs/Account/AniListUpdateDto.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace API.DTOs.Account;
|
||||
|
||||
public class AniListUpdateDto
|
||||
{
|
||||
public string Token { get; set; }
|
||||
}
|
7
API/DTOs/Account/LicenseValidDto.cs
Normal file
7
API/DTOs/Account/LicenseValidDto.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace API.DTOs.Account;
|
||||
|
||||
public class LicenseValidDto
|
||||
{
|
||||
public required string License { get; set; }
|
||||
public required string InstallId { get; set; }
|
||||
}
|
|
@ -4,4 +4,8 @@ public class LoginDto
|
|||
{
|
||||
public string Username { get; init; } = default!;
|
||||
public string Password { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// If ApiKey is passed, will ignore username/password for validation
|
||||
/// </summary>
|
||||
public string? ApiKey { get; set; } = default!;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace API.DTOs;
|
|||
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
|
||||
/// file (abstracted from type).
|
||||
/// </summary>
|
||||
public class ChapterDto : IHasReadTimeEstimate, IEntityDate
|
||||
public class ChapterDto : IHasReadTimeEstimate
|
||||
{
|
||||
public int Id { get; init; }
|
||||
/// <summary>
|
||||
|
@ -45,6 +45,10 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate
|
|||
/// </summary>
|
||||
public DateTime LastReadingProgressUtc { get; set; }
|
||||
/// <summary>
|
||||
/// The last time a chapter was read by current authenticated user
|
||||
/// </summary>
|
||||
public DateTime LastReadingProgress { get; set; }
|
||||
/// <summary>
|
||||
/// If the Cover Image is locked for this entity
|
||||
/// </summary>
|
||||
public bool CoverImageLocked { get; set; }
|
||||
|
@ -55,11 +59,14 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate
|
|||
/// <summary>
|
||||
/// When chapter was created
|
||||
/// </summary>
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
/// <summary>
|
||||
/// When chapter was created in local server time
|
||||
/// </summary>
|
||||
/// <remarks>This is required for Tachiyomi Extension</remarks>
|
||||
public DateTime Created { get; set; }
|
||||
/// <summary>
|
||||
/// When the chapter was released.
|
||||
/// </summary>
|
||||
/// <remarks>Metadata field</remarks>
|
||||
|
@ -93,4 +100,13 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate
|
|||
public int MaxHoursToRead { get; set; }
|
||||
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
||||
public int AvgHoursToRead { get; set; }
|
||||
/// <summary>
|
||||
/// Comma-separated link of urls to external services that have some relation to the Chapter
|
||||
/// </summary>
|
||||
public string WebLinks { get; set; }
|
||||
/// <summary>
|
||||
/// ISBN-13 (usually) of the Chapter
|
||||
/// </summary>
|
||||
/// <remarks>This is guaranteed to be Valid</remarks>
|
||||
public string ISBN { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs;
|
||||
|
||||
public class CreateLibraryDto
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; init; } = default!;
|
||||
[Required]
|
||||
public LibraryType Type { get; init; }
|
||||
[Required]
|
||||
[MinLength(1)]
|
||||
public IEnumerable<string> Folders { get; init; } = default!;
|
||||
}
|
|
@ -26,12 +26,4 @@ public class DeviceDto
|
|||
/// Platform (ie) Windows 10
|
||||
/// </summary>
|
||||
public DevicePlatform Platform { get; set; }
|
||||
/// <summary>
|
||||
/// Last time this device was used to send a file
|
||||
/// </summary>
|
||||
public DateTime LastUsed { get; set; }
|
||||
/// <summary>
|
||||
/// Last time this device was used to send a file
|
||||
/// </summary>
|
||||
public DateTime LastUsedUtc { get; set; }
|
||||
}
|
||||
|
|
7
API/DTOs/Device/SendSeriesToDeviceDto.cs
Normal file
7
API/DTOs/Device/SendSeriesToDeviceDto.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace API.DTOs.Device;
|
||||
|
||||
public class SendSeriesToDeviceDto
|
||||
{
|
||||
public int DeviceId { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
}
|
7
API/DTOs/Filtering/v2/FilterCombination.cs
Normal file
7
API/DTOs/Filtering/v2/FilterCombination.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace API.DTOs.Filtering.v2;
|
||||
|
||||
public enum FilterCombination
|
||||
{
|
||||
Or = 0,
|
||||
And = 1
|
||||
}
|
56
API/DTOs/Filtering/v2/FilterComparision.cs
Normal file
56
API/DTOs/Filtering/v2/FilterComparision.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace API.DTOs.Filtering.v2;
|
||||
|
||||
public enum FilterComparison
|
||||
{
|
||||
[Description("Equal")]
|
||||
Equal = 0,
|
||||
GreaterThan = 1,
|
||||
GreaterThanEqual = 2,
|
||||
LessThan = 3,
|
||||
LessThanEqual = 4,
|
||||
/// <summary>
|
||||
/// value is within any of the series. This is inheritently an OR, even if combinator is an AND
|
||||
/// </summary>
|
||||
/// <remarks>Only works with IList</remarks>
|
||||
Contains = 5,
|
||||
/// <summary>
|
||||
/// value is within All of the series. This is an AND, even if combinator ORs the different statements
|
||||
/// </summary>
|
||||
/// <remarks>Only works with IList</remarks>
|
||||
MustContains = 6,
|
||||
/// <summary>
|
||||
/// Performs a LIKE %value%
|
||||
/// </summary>
|
||||
Matches = 7,
|
||||
NotContains = 8,
|
||||
/// <summary>
|
||||
/// Not Equal to
|
||||
/// </summary>
|
||||
NotEqual = 9,
|
||||
/// <summary>
|
||||
/// String starts with
|
||||
/// </summary>
|
||||
BeginsWith = 10,
|
||||
/// <summary>
|
||||
/// String ends with
|
||||
/// </summary>
|
||||
EndsWith = 11,
|
||||
/// <summary>
|
||||
/// Is Date before X
|
||||
/// </summary>
|
||||
IsBefore = 12,
|
||||
/// <summary>
|
||||
/// Is Date after X
|
||||
/// </summary>
|
||||
IsAfter = 13,
|
||||
/// <summary>
|
||||
/// Is Date between now and X seconds ago
|
||||
/// </summary>
|
||||
IsInLast = 14,
|
||||
/// <summary>
|
||||
/// Is Date not between now and X seconds ago
|
||||
/// </summary>
|
||||
IsNotInLast = 15,
|
||||
}
|
40
API/DTOs/Filtering/v2/FilterField.cs
Normal file
40
API/DTOs/Filtering/v2/FilterField.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
namespace API.DTOs.Filtering.v2;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the field which will dictate the value type and the Extension used for filtering
|
||||
/// </summary>
|
||||
public enum FilterField
|
||||
{
|
||||
Summary = 0,
|
||||
SeriesName = 1,
|
||||
PublicationStatus = 2,
|
||||
Languages = 3,
|
||||
AgeRating = 4,
|
||||
UserRating = 5,
|
||||
Tags = 6,
|
||||
CollectionTags = 7,
|
||||
Translators = 8,
|
||||
Characters = 9,
|
||||
Publisher = 10,
|
||||
Editor = 11,
|
||||
CoverArtist = 12,
|
||||
Letterer = 13,
|
||||
Colorist = 14,
|
||||
Inker = 15,
|
||||
Penciller = 16,
|
||||
Writers = 17,
|
||||
Genres = 18,
|
||||
Libraries = 19,
|
||||
ReadProgress = 20,
|
||||
Formats = 21,
|
||||
ReleaseYear = 22,
|
||||
ReadTime = 23,
|
||||
/// <summary>
|
||||
/// Series Folder
|
||||
/// </summary>
|
||||
Path = 24,
|
||||
/// <summary>
|
||||
/// File path
|
||||
/// </summary>
|
||||
FilePath = 25
|
||||
}
|
8
API/DTOs/Filtering/v2/FilterStatementDto.cs
Normal file
8
API/DTOs/Filtering/v2/FilterStatementDto.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace API.DTOs.Filtering.v2;
|
||||
|
||||
public class FilterStatementDto
|
||||
{
|
||||
public FilterComparison Comparison { get; set; }
|
||||
public FilterField Field { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
30
API/DTOs/Filtering/v2/FilterV2Dto.cs
Normal file
30
API/DTOs/Filtering/v2/FilterV2Dto.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
|
||||
namespace API.DTOs.Filtering.v2;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Metadata filtering for v2 API only
|
||||
/// </summary>
|
||||
public class FilterV2Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the filter
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
public ICollection<FilterStatementDto> Statements { get; set; } = new List<FilterStatementDto>();
|
||||
public FilterCombination Combination { get; set; } = FilterCombination.And;
|
||||
public SortOptions SortOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Limit the number of rows returned. Defaults to not applying a limit (aka 0)
|
||||
/// </summary>
|
||||
public int LimitTo { get; set; } = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -15,14 +15,6 @@ public class JobDto
|
|||
/// <summary>
|
||||
/// When the job was created
|
||||
/// </summary>
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// Last time the job was run
|
||||
/// </summary>
|
||||
public DateTime? LastExecution { get; set; }
|
||||
/// <summary>
|
||||
/// When the job was created
|
||||
/// </summary>
|
||||
public DateTime? CreatedAtUtc { get; set; }
|
||||
/// <summary>
|
||||
/// Last time the job was run
|
||||
|
|
|
@ -41,6 +41,11 @@ public class LibraryDto
|
|||
/// Include library series in Search
|
||||
/// </summary>
|
||||
public bool IncludeInSearch { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Should this library allow Scrobble events to emit from it
|
||||
/// </summary>
|
||||
/// <remarks>Scrobbling requires a valid LicenseKey</remarks>
|
||||
public bool AllowScrobbling { get; set; } = true;
|
||||
public ICollection<string> Folders { get; init; } = new List<string>();
|
||||
/// <summary>
|
||||
/// When showing series, only parent series or series with no relationships will be returned
|
||||
|
|
8
API/DTOs/License/EncryptLicenseDto.cs
Normal file
8
API/DTOs/License/EncryptLicenseDto.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace API.DTOs.License;
|
||||
|
||||
public class EncryptLicenseDto
|
||||
{
|
||||
public required string License { get; set; }
|
||||
public required string InstallId { get; set; }
|
||||
public required string EmailId { get; set; }
|
||||
}
|
13
API/DTOs/License/UpdateLicenseDto.cs
Normal file
13
API/DTOs/License/UpdateLicenseDto.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace API.DTOs.License;
|
||||
|
||||
public class UpdateLicenseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// License Key received from Kavita+
|
||||
/// </summary>
|
||||
public required string License { get; set; }
|
||||
/// <summary>
|
||||
/// Email registered with Stripe
|
||||
/// </summary>
|
||||
public required string Email { get; set; }
|
||||
}
|
23
API/DTOs/MediaErrors/MediaErrorDto.cs
Normal file
23
API/DTOs/MediaErrors/MediaErrorDto.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
|
||||
namespace API.DTOs.MediaErrors;
|
||||
|
||||
public class MediaErrorDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Format Type (RAR, ZIP, 7Zip, Epub, PDF)
|
||||
/// </summary>
|
||||
public required string Extension { get; set; }
|
||||
/// <summary>
|
||||
/// Full Filepath to the file that has some issue
|
||||
/// </summary>
|
||||
public required string FilePath { get; set; }
|
||||
/// <summary>
|
||||
/// Developer defined string
|
||||
/// </summary>
|
||||
public string Comment { get; set; }
|
||||
/// <summary>
|
||||
/// Exception message
|
||||
/// </summary>
|
||||
public string Details { get; set; }
|
||||
}
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Xml.Serialization;
|
||||
|
||||
namespace API.DTOs.OPDS;
|
||||
#nullable enable
|
||||
|
||||
public class FeedEntry
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
#nullable enable
|
||||
|
||||
public class ProgressDto
|
||||
{
|
||||
|
|
11
API/DTOs/RatingDto.cs
Normal file
11
API/DTOs/RatingDto.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using API.Services.Plus;
|
||||
|
||||
namespace API.DTOs;
|
||||
|
||||
public class RatingDto
|
||||
{
|
||||
public int AverageScore { get; set; }
|
||||
public int FavoriteCount { get; set; }
|
||||
public ScrobbleProvider Provider { get; set; }
|
||||
public string? ProviderUrl { get; set; }
|
||||
}
|
|
@ -13,4 +13,8 @@ public class BookmarkDto
|
|||
public int SeriesId { get; set; }
|
||||
[Required]
|
||||
public int ChapterId { get; set; }
|
||||
/// <summary>
|
||||
/// This is only used when getting all bookmarks.
|
||||
/// </summary>
|
||||
public SeriesDto? Series { get; set; }
|
||||
}
|
||||
|
|
12
API/DTOs/Reader/CreatePersonalToCDto.cs
Normal file
12
API/DTOs/Reader/CreatePersonalToCDto.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace API.DTOs.Reader;
|
||||
|
||||
public class CreatePersonalToCDto
|
||||
{
|
||||
public required int ChapterId { get; set; }
|
||||
public required int VolumeId { get; set; }
|
||||
public required int SeriesId { get; set; }
|
||||
public required int LibraryId { get; set; }
|
||||
public required int PageNumber { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? BookScrollId { get; set; }
|
||||
}
|
9
API/DTOs/Reader/PersonalToCDto.cs
Normal file
9
API/DTOs/Reader/PersonalToCDto.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace API.DTOs.Reader;
|
||||
|
||||
public class PersonalToCDto
|
||||
{
|
||||
public required int ChapterId { get; set; }
|
||||
public required int PageNumber { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? BookScrollId { get; set; }
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.ReadingLists;
|
||||
#nullable enable
|
||||
|
||||
public class ReadingListItemDto
|
||||
{
|
||||
|
|
10
API/DTOs/Recommendation/ExternalSeriesDto.cs
Normal file
10
API/DTOs/Recommendation/ExternalSeriesDto.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace API.DTOs.Recommendation;
|
||||
#nullable enable
|
||||
|
||||
public class ExternalSeriesDto
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string CoverUrl { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
}
|
9
API/DTOs/Recommendation/RecommendationDto.cs
Normal file
9
API/DTOs/Recommendation/RecommendationDto.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.Recommendation;
|
||||
|
||||
public class RecommendationDto
|
||||
{
|
||||
public IList<SeriesDto> OwnedSeries { get; set; } = new List<SeriesDto>();
|
||||
public IList<ExternalSeriesDto> ExternalSeries { get; set; } = new List<ExternalSeriesDto>();
|
||||
}
|
81
API/DTOs/Scrobbling/ScrobbleDto.cs
Normal file
81
API/DTOs/Scrobbling/ScrobbleDto.cs
Normal file
|
@ -0,0 +1,81 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace API.DTOs.Scrobbling;
|
||||
#nullable enable
|
||||
|
||||
public enum ScrobbleEventType
|
||||
{
|
||||
[Description("Chapter Read")]
|
||||
ChapterRead = 0,
|
||||
[Description("Add to Want to Read")]
|
||||
AddWantToRead = 1,
|
||||
[Description("Remove from Want to Read")]
|
||||
RemoveWantToRead = 2,
|
||||
[Description("Score Updated")]
|
||||
ScoreUpdated = 3,
|
||||
[Description("Review Added/Updated")]
|
||||
Review = 4
|
||||
}
|
||||
|
||||
public enum MediaFormat
|
||||
{
|
||||
Manga = 1,
|
||||
Comic = 2,
|
||||
LightNovel = 3,
|
||||
Book = 4
|
||||
}
|
||||
|
||||
|
||||
public class ScrobbleDto
|
||||
{
|
||||
/// <summary>
|
||||
/// User's access token to allow us to talk on their behalf
|
||||
/// </summary>
|
||||
public string AniListToken { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string LocalizedSeriesName { get; set; }
|
||||
public MediaFormat Format { get; set; }
|
||||
public int? Year { get; set; }
|
||||
/// <summary>
|
||||
/// Optional AniListId if present on Kavita's WebLinks
|
||||
/// </summary>
|
||||
public int? AniListId { get; set; } = 0;
|
||||
public int? MALId { get; set; } = 0;
|
||||
public string BakaUpdatesId { get; set; } = string.Empty;
|
||||
|
||||
public ScrobbleEventType ScrobbleEventType { get; set; }
|
||||
/// <summary>
|
||||
/// Number of chapters read
|
||||
/// </summary>
|
||||
/// <remarks>If completed series, this can consider the Series Read (AniList)</remarks>
|
||||
public int? ChapterNumber { get; set; }
|
||||
/// <summary>
|
||||
/// Number of Volumes read
|
||||
/// </summary>
|
||||
/// <remarks>This will not consider the series Completed, even if all Volumes have been read (AniList)</remarks>
|
||||
public int? VolumeNumber { get; set; }
|
||||
/// <summary>
|
||||
/// Rating for the Series
|
||||
/// </summary>
|
||||
public float? Rating { get; set; }
|
||||
public string? ReviewTitle { get; set; }
|
||||
public string? ReviewBody { get; set; }
|
||||
/// <summary>
|
||||
/// The date that the series was started reading. Will be null for non ReadingProgress events
|
||||
/// </summary>
|
||||
public DateTime? StartedReadingDateUtc { get; set; }
|
||||
/// <summary>
|
||||
/// The latest date the series was read. Will be null for non ReadingProgress events
|
||||
/// </summary>
|
||||
public DateTime? LatestReadingDateUtc { get; set; }
|
||||
/// <summary>
|
||||
/// The date that the series was scrobbled. Will be null for non ReadingProgress events
|
||||
/// </summary>
|
||||
public DateTime? ScrobbleDateUtc { get; set; }
|
||||
/// <summary>
|
||||
/// Optional but can help with matching
|
||||
/// </summary>
|
||||
public string? Isbn { get; set; }
|
||||
|
||||
}
|
18
API/DTOs/Scrobbling/ScrobbleErrorDto.cs
Normal file
18
API/DTOs/Scrobbling/ScrobbleErrorDto.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
|
||||
namespace API.DTOs.Scrobbling;
|
||||
|
||||
public class ScrobbleErrorDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Developer defined string
|
||||
/// </summary>
|
||||
public string Comment { get; set; }
|
||||
/// <summary>
|
||||
/// List of providers that could not
|
||||
/// </summary>
|
||||
public string Details { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
}
|
18
API/DTOs/Scrobbling/ScrobbleEventDto.cs
Normal file
18
API/DTOs/Scrobbling/ScrobbleEventDto.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
|
||||
namespace API.DTOs.Scrobbling;
|
||||
|
||||
public class ScrobbleEventDto
|
||||
{
|
||||
public string SeriesName { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public bool IsProcessed { get; set; }
|
||||
public int? VolumeNumber { get; set; }
|
||||
public int? ChapterNumber { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public float? Rating { get; set; }
|
||||
public ScrobbleEventType ScrobbleEventType { get; set; }
|
||||
|
||||
}
|
12
API/DTOs/Scrobbling/ScrobbleHoldDto.cs
Normal file
12
API/DTOs/Scrobbling/ScrobbleHoldDto.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace API.DTOs.Scrobbling;
|
||||
|
||||
public class ScrobbleHoldDto
|
||||
{
|
||||
public string SeriesName { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
}
|
11
API/DTOs/Scrobbling/ScrobbleResponseDto.cs
Normal file
11
API/DTOs/Scrobbling/ScrobbleResponseDto.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace API.DTOs.Scrobbling;
|
||||
|
||||
/// <summary>
|
||||
/// Response from Kavita+ Scrobble API
|
||||
/// </summary>
|
||||
public class ScrobbleResponseDto
|
||||
{
|
||||
public bool Successful { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public int RateLeft { get; set; }
|
||||
}
|
11
API/DTOs/SeriesDetail/UpdateUserReviewDto.cs
Normal file
11
API/DTOs/SeriesDetail/UpdateUserReviewDto.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs.SeriesDetail;
|
||||
|
||||
public class UpdateUserReviewDto
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
[MaxLength(120)]
|
||||
public string? Tagline { get; set; }
|
||||
public string Body { get; set; }
|
||||
}
|
58
API/DTOs/SeriesDetail/UserReviewDto.cs
Normal file
58
API/DTOs/SeriesDetail/UserReviewDto.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using API.Services.Plus;
|
||||
|
||||
namespace API.DTOs.SeriesDetail;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a User Review for a given Series
|
||||
/// </summary>
|
||||
/// <remarks>The user does not need to be a Kavita user</remarks>
|
||||
public class UserReviewDto
|
||||
{
|
||||
/// <summary>
|
||||
/// A tagline for the review
|
||||
/// </summary>
|
||||
public string? Tagline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The main review
|
||||
/// </summary>
|
||||
public string Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The series this is for
|
||||
/// </summary>
|
||||
public int SeriesId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The library this series belongs in
|
||||
/// </summary>
|
||||
public int LibraryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user who wrote this
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many upvotes this review has gotten
|
||||
/// </summary>
|
||||
/// <remarks>More upvotes get loaded first</remarks>
|
||||
public int Score { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// If External, the url of the review
|
||||
/// </summary>
|
||||
public string? ExternalUrl { get; set; }
|
||||
/// <summary>
|
||||
/// Does this review come from an external Source
|
||||
/// </summary>
|
||||
public bool IsExternal { get; set; }
|
||||
/// <summary>
|
||||
/// The main body with just text, for review preview
|
||||
/// </summary>
|
||||
public string? BodyJustText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If this review is External, which Provider did it come from
|
||||
/// </summary>
|
||||
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.Kavita;
|
||||
}
|
|
@ -3,6 +3,7 @@ using API.Entities.Enums;
|
|||
using API.Entities.Interfaces;
|
||||
|
||||
namespace API.DTOs;
|
||||
#nullable enable
|
||||
|
||||
public class SeriesDto : IHasReadTimeEstimate
|
||||
{
|
||||
|
@ -11,7 +12,6 @@ public class SeriesDto : IHasReadTimeEstimate
|
|||
public string? OriginalName { get; init; }
|
||||
public string? LocalizedName { get; init; }
|
||||
public string? SortName { get; init; }
|
||||
public string? Summary { get; init; }
|
||||
public int Pages { get; init; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
/// <summary>
|
||||
|
@ -29,13 +29,13 @@ public class SeriesDto : IHasReadTimeEstimate
|
|||
/// <summary>
|
||||
/// Rating from logged in user. Calculated at API-time.
|
||||
/// </summary>
|
||||
public int UserRating { get; set; }
|
||||
public float UserRating { get; set; }
|
||||
/// <summary>
|
||||
/// Review from logged in user. Calculated at API-time.
|
||||
/// If the user has set the rating or not
|
||||
/// </summary>
|
||||
public string? UserReview { get; set; }
|
||||
public MangaFormat Format { get; set; }
|
||||
public bool HasUserRated { get; set; }
|
||||
|
||||
public MangaFormat Format { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
|
||||
public bool NameLocked { get; set; }
|
||||
|
|
|
@ -58,6 +58,10 @@ public class SeriesMetadataDto
|
|||
/// Publication status of the Series
|
||||
/// </summary>
|
||||
public PublicationStatus PublicationStatus { get; set; }
|
||||
/// <summary>
|
||||
/// A comma-separated list of Urls
|
||||
/// </summary>
|
||||
public string WebLinks { get; set; }
|
||||
|
||||
public bool LanguageLocked { get; set; }
|
||||
public bool SummaryLocked { get; set; }
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using API.Services;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
|
||||
namespace API.DTOs.Settings;
|
||||
|
||||
public class ServerSettingDto
|
||||
{
|
||||
|
||||
public string CacheDirectory { get; set; } = default!;
|
||||
public string TaskScan { get; set; } = default!;
|
||||
/// <summary>
|
||||
|
@ -47,9 +49,11 @@ public class ServerSettingDto
|
|||
/// </summary>
|
||||
public string InstallId { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// If the server should save bookmarks as WebP encoding
|
||||
/// The format that should be used when saving media for Kavita
|
||||
/// </summary>
|
||||
public bool ConvertBookmarkToWebP { get; set; }
|
||||
/// <example>This includes things like: Covers, Bookmarks, Favicons</example>
|
||||
public EncodeFormat EncodeMediaAs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of Backups before cleanup
|
||||
/// </summary>
|
||||
|
@ -65,11 +69,23 @@ public class ServerSettingDto
|
|||
/// <remarks>Value should be between 1 and 30</remarks>
|
||||
public int TotalLogs { get; set; }
|
||||
/// <summary>
|
||||
/// If the server should save covers as WebP encoding
|
||||
/// </summary>
|
||||
public bool ConvertCoverToWebP { get; set; }
|
||||
/// <summary>
|
||||
/// The Host name (ie Reverse proxy domain name) for the server
|
||||
/// </summary>
|
||||
public string HostName { get; set; }
|
||||
/// <summary>
|
||||
/// The size in MB for Caching API data
|
||||
/// </summary>
|
||||
public long CacheSize { get; set; }
|
||||
/// <summary>
|
||||
/// How many Days since today in the past for reading progress, should content be considered for On Deck, before it gets removed automatically
|
||||
/// </summary>
|
||||
public int OnDeckProgressDays { get; set; }
|
||||
/// <summary>
|
||||
/// How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically
|
||||
/// </summary>
|
||||
public int OnDeckUpdateDays { get; set; }
|
||||
/// <summary>
|
||||
/// How large the cover images should be
|
||||
/// </summary>
|
||||
public CoverImageSize CoverImageSize { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.Statistics;
|
||||
#nullable enable
|
||||
|
||||
public class ServerStatisticsDto
|
||||
{
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Stats;
|
||||
|
@ -85,11 +86,6 @@ public class ServerInfoDto
|
|||
/// <remarks>Introduced in v0.5.4</remarks>
|
||||
public int TotalPeople { get; set; }
|
||||
/// <summary>
|
||||
/// Is this instance storing bookmarks as WebP
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.5.4</remarks>
|
||||
public bool StoreBookmarksAsWebP { get; set; }
|
||||
/// <summary>
|
||||
/// Number of users on this instance using Card Layout
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.5.4</remarks>
|
||||
|
@ -175,8 +171,13 @@ public class ServerInfoDto
|
|||
/// <remarks>Introduced in v0.7.0</remarks>
|
||||
public long TotalReadingHours { get; set; }
|
||||
/// <summary>
|
||||
/// Is the Server saving covers as WebP
|
||||
/// The encoding the server is using to save media
|
||||
/// </summary>
|
||||
/// <remarks>Added in v0.7.0</remarks>
|
||||
public bool StoreCoversAsWebP { get; set; }
|
||||
/// <remarks>Added in v0.7.3</remarks>
|
||||
public EncodeFormat EncodeMediaAs { get; set; }
|
||||
/// <summary>
|
||||
/// The last user reading progress on the server (in UTC)
|
||||
/// </summary>
|
||||
/// <remarks>Added in v0.7.4</remarks>
|
||||
public DateTime LastReadTime { get; set; }
|
||||
}
|
||||
|
|
21
API/DTOs/Stats/ServerInfoSlimDto.cs
Normal file
21
API/DTOs/Stats/ServerInfoSlimDto.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace API.DTOs.Stats;
|
||||
|
||||
/// <summary>
|
||||
/// This is just for the Server tab on UI
|
||||
/// </summary>
|
||||
public class ServerInfoSlimDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique Id that represents a unique install
|
||||
/// </summary>
|
||||
public required string InstallId { get; set; }
|
||||
/// <summary>
|
||||
/// If the Kavita install is using Docker
|
||||
/// </summary>
|
||||
public bool IsDocker { get; set; }
|
||||
/// <summary>
|
||||
/// Version of Kavita
|
||||
/// </summary>
|
||||
public required string KavitaVersion { get; set; }
|
||||
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
using System;
|
||||
using API.Entities.Enums.Theme;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Services;
|
||||
|
||||
namespace API.DTOs.Theme;
|
||||
|
@ -8,7 +6,7 @@ namespace API.DTOs.Theme;
|
|||
/// <summary>
|
||||
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
|
||||
/// </summary>
|
||||
public class SiteThemeDto : IEntityDate
|
||||
public class SiteThemeDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
|
@ -32,9 +30,5 @@ public class SiteThemeDto : IEntityDate
|
|||
/// Where did the theme come from
|
||||
/// </summary>
|
||||
public ThemeProvider Provider { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
public string Selector => "bg-" + Name.ToLower();
|
||||
}
|
||||
|
|
|
@ -26,4 +26,6 @@ public class UpdateLibraryDto
|
|||
public bool ManageCollections { get; init; }
|
||||
[Required]
|
||||
public bool ManageReadingLists { get; init; }
|
||||
[Required]
|
||||
public bool AllowScrobbling { get; init; }
|
||||
}
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
public class UpdateSeriesDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? LocalizedName { get; init; }
|
||||
public string? SortName { get; init; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
|
||||
public bool NameLocked { get; set; }
|
||||
public bool SortNameLocked { get; set; }
|
||||
public bool LocalizedNameLocked { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
namespace API.DTOs;
|
||||
|
||||
public class UpdateSeriesRatingDto
|
||||
{
|
||||
public int SeriesId { get; init; }
|
||||
public int UserRating { get; init; }
|
||||
[MaxLength(1000)]
|
||||
public string? UserReview { get; init; }
|
||||
public float UserRating { get; init; }
|
||||
}
|
||||
|
|
|
@ -142,4 +142,14 @@ public class UserPreferencesDto
|
|||
/// </summary>
|
||||
[Required]
|
||||
public bool CollapseSeriesRelationships { get; set; } = false;
|
||||
/// <summary>
|
||||
/// UI Site Global Setting: Should series reviews be shared with all users in the server
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool ShareReviews { get; set; } = false;
|
||||
/// <summary>
|
||||
/// UI Site Global Setting: The language locale that should be used for the user
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Locale { get; set; }
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue