Merged develop in
This commit is contained in:
commit
a443be7523
322 changed files with 31244 additions and 6350 deletions
49
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
49
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -1,24 +1,17 @@
|
|||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: ""
|
||||
description: Help us make Kavita better for everyone by submitting issues you run into while using the program.
|
||||
title: "Put a short summary of what went wrong here"
|
||||
labels: ["needs-triage"]
|
||||
assignees:
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If you have a feature request, please submit on [Github Discussions](https://github.com/Kareadita/Kavita/discussions/2529).
|
||||
value: "Thanks for taking the time to fill out this bug report!"
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what steps you took so we can try to reproduce.
|
||||
description: Don't forget to tell us what steps you took so we can try to reproduce.
|
||||
placeholder: Tell us what you see!
|
||||
value: ""
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
@ -26,33 +19,35 @@ body:
|
|||
attributes:
|
||||
label: What did you expect?
|
||||
description: What did you expect to happen?
|
||||
placeholder: Tell us what you expected to see!
|
||||
value: ""
|
||||
placeholder: Tell us what you expected to see! Go in as much detail as possible so we can confirm if the behavior is something that is broken.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version are you running?
|
||||
placeholder: Can be found by going to Server Settings > System
|
||||
value: ""
|
||||
label: Kavita Version Number - Don't see your version number listed? Then your install is out of date. Please update and see if your issue still persists.
|
||||
multiple: false
|
||||
options:
|
||||
- 0.7.14 - Stable
|
||||
- Nightly Testing Branch
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: OS
|
||||
attributes:
|
||||
label: What OS is Kavita being run on?
|
||||
label: What operating system is Kavita being hosted from?
|
||||
multiple: false
|
||||
options:
|
||||
- Docker
|
||||
- Docker (LSIO Container)
|
||||
- Docker (Dockerhub Container)
|
||||
- Docker (Other)
|
||||
- Windows
|
||||
- Linux
|
||||
- Mac
|
||||
- type: dropdown
|
||||
id: desktop-OS
|
||||
attributes:
|
||||
label: If issue being seen on Desktop, what OS are you running where you see the issue?
|
||||
label: If the issue is being seen on Desktop, what OS are you running where you see the issue?
|
||||
multiple: false
|
||||
options:
|
||||
- Windows
|
||||
|
@ -61,17 +56,18 @@ body:
|
|||
- type: dropdown
|
||||
id: desktop-browsers
|
||||
attributes:
|
||||
label: If issue being seen in the UI, what browsers are you seeing the problem on?
|
||||
label: If the issue is being seen in the UI, what browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other (List in "Additional Notes" box)
|
||||
- type: dropdown
|
||||
id: mobile-OS
|
||||
attributes:
|
||||
label: If issue being seen on Mobile, what OS are you running where you see the issue?
|
||||
label: If the issue is being seen on Mobile, what OS are you running where you see the issue?
|
||||
multiple: false
|
||||
options:
|
||||
- Android
|
||||
|
@ -79,7 +75,7 @@ body:
|
|||
- type: dropdown
|
||||
id: mobile-browsers
|
||||
attributes:
|
||||
label: If issue being seen on UI, what browsers are you seeing the problem on?
|
||||
label: If the issue is being seen on the UI, what browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
|
@ -97,7 +93,4 @@ body:
|
|||
attributes:
|
||||
label: Additional Notes
|
||||
description: Any other information about the issue not covered in this form?
|
||||
placeholder: e.g. Running Kavita on a raspberry pi, updating from X version, using LSIO container, etc
|
||||
value: ""
|
||||
validations:
|
||||
required: true
|
||||
placeholder: e.g. Running Kavita on a Raspberry Pi, updating from X version, using LSIO container, etc
|
||||
|
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Requests
|
||||
url: https://github.com/Kareadita/Kavita/discussions
|
||||
about: Suggest an idea for the Kavita project
|
||||
|
|
38
.github/workflows/build-and-test.yml
vendored
38
.github/workflows/build-and-test.yml
vendored
|
@ -26,48 +26,10 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: csproj
|
||||
path: Kavita.Common/Kavita.Common.csproj
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~\sonar\cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -Path .\.sonar\scanner -ItemType Directory
|
||||
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
||||
|
||||
- name: Sonar Scan
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"Kareadita_Kavita" /o:"kareadita" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
|
||||
dotnet build --configuration Release
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --no-restore --verbosity normal
|
||||
|
|
14
.github/workflows/release-workflow.yml
vendored
14
.github/workflows/release-workflow.yml
vendored
|
@ -59,6 +59,13 @@ jobs:
|
|||
id: parse-body
|
||||
run: |
|
||||
body="${{ steps.findPr.outputs.body }}"
|
||||
body=${body//\'/}
|
||||
body=${body//'%'/'%25'}
|
||||
body=${body//$'\n'/'%0A'}
|
||||
body=${body//$'\r'/'%0D'}
|
||||
body=${body//$'`'/'%60'}
|
||||
body=${body//$'>'/'%3E'}
|
||||
|
||||
if [[ ${#body} -gt 1870 ]] ; then
|
||||
body=${body:0:1870}
|
||||
body="${body}...and much more.
|
||||
|
@ -66,16 +73,9 @@ jobs:
|
|||
Read full changelog: https://github.com/Kareadita/Kavita/releases/latest"
|
||||
fi
|
||||
|
||||
body=${body//\'/}
|
||||
body=${body//'%'/'%25'}
|
||||
body=${body//$'\n'/'%0A'}
|
||||
body=${body//$'\r'/'%0D'}
|
||||
body=${body//$'`'/'%60'}
|
||||
body=${body//$'>'/'%3E'}
|
||||
echo $body
|
||||
echo "BODY=$body" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -527,8 +527,7 @@ API/config/stats/*
|
|||
API/config/stats/app_stats.json
|
||||
API/config/pre-metadata/
|
||||
API/config/post-metadata/
|
||||
API/config/relations-imported.csv
|
||||
API/config/relations.csv
|
||||
API/config/*.csv
|
||||
API.Tests/TestResults/
|
||||
UI/Web/.vscode/settings.json
|
||||
/API.Tests/Services/Test Data/ArchiveService/CoverImages/output/*
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.11" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.11" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.12" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ public class TestBenchmark
|
|||
{
|
||||
list.Add(new VolumeDto()
|
||||
{
|
||||
Number = random.Next(10) > 5 ? 1 : 0,
|
||||
MinNumber = random.Next(10) > 5 ? 1 : 0,
|
||||
Chapters = GenerateChapters()
|
||||
});
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ public class TestBenchmark
|
|||
|
||||
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||
{
|
||||
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
|
||||
foreach (var v in volumes.Where(vDto => vDto.MinNumber == 0))
|
||||
{
|
||||
v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList();
|
||||
}
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.4" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.4" />
|
||||
<PackageReference Include="xunit" Version="2.6.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5">
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.15" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.15" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
using API.Helpers.Converters;
|
||||
using Hangfire;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Converters;
|
||||
|
||||
#nullable enable
|
||||
public class CronConverterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("daily", "0 0 * * *")]
|
||||
[InlineData("disabled", "0 0 31 2 *")]
|
||||
[InlineData("weekly", "0 0 * * 1")]
|
||||
[InlineData("", "0 0 31 2 *")]
|
||||
[InlineData("sdfgdf", "")]
|
||||
public void ConvertTest(string input, string expected)
|
||||
[InlineData("0 0 31 2 *", "0 0 31 2 *")]
|
||||
[InlineData("sdfgdf", "sdfgdf")]
|
||||
[InlineData("* * * * *", "* * * * *")]
|
||||
[InlineData(null, "0 0 * * *")] // daily
|
||||
public void ConvertTest(string? input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
|
||||
}
|
||||
|
|
|
@ -192,7 +192,7 @@ public class SeriesExtensionsTests
|
|||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 1")
|
||||
|
@ -229,7 +229,7 @@ public class SeriesExtensionsTests
|
|||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 1")
|
||||
|
@ -266,14 +266,14 @@ public class SeriesExtensionsTests
|
|||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 1")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("137")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 137")
|
||||
|
@ -306,7 +306,7 @@ public class SeriesExtensionsTests
|
|||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("4")
|
||||
.WithNumber(4)
|
||||
.WithMinNumber(4)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 4")
|
||||
|
|
|
@ -27,7 +27,7 @@ public class VolumeListExtensionsTests
|
|||
.Build(),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[0].Number, volumes.GetCoverImage(MangaFormat.Archive).Number);
|
||||
Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
79
API.Tests/Helpers/RateLimiterTests.cs
Normal file
79
API.Tests/Helpers/RateLimiterTests.cs
Normal file
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using API.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
|
||||
public class RateLimiterTests
|
||||
{
|
||||
[Fact]
|
||||
public void AcquireTokens_Successful()
|
||||
{
|
||||
// Arrange
|
||||
var limiter = new RateLimiter(3, TimeSpan.FromSeconds(1));
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(limiter.TryAcquire("test_key"));
|
||||
Assert.True(limiter.TryAcquire("test_key"));
|
||||
Assert.True(limiter.TryAcquire("test_key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcquireTokens_ExceedLimit()
|
||||
{
|
||||
// Arrange
|
||||
var limiter = new RateLimiter(2, TimeSpan.FromSeconds(10), false);
|
||||
|
||||
// Act
|
||||
limiter.TryAcquire("test_key");
|
||||
limiter.TryAcquire("test_key");
|
||||
|
||||
// Assert
|
||||
Assert.False(limiter.TryAcquire("test_key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcquireTokens_Refill()
|
||||
{
|
||||
// Arrange
|
||||
var limiter = new RateLimiter(2, TimeSpan.FromSeconds(1));
|
||||
|
||||
// Act
|
||||
limiter.TryAcquire("test_key");
|
||||
limiter.TryAcquire("test_key");
|
||||
|
||||
// Wait for refill
|
||||
System.Threading.Thread.Sleep(1100);
|
||||
|
||||
// Assert
|
||||
Assert.True(limiter.TryAcquire("test_key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcquireTokens_Refill_WithOff()
|
||||
{
|
||||
// Arrange
|
||||
var limiter = new RateLimiter(2, TimeSpan.FromSeconds(10), false);
|
||||
|
||||
// Act
|
||||
limiter.TryAcquire("test_key");
|
||||
limiter.TryAcquire("test_key");
|
||||
|
||||
// Wait for refill
|
||||
System.Threading.Thread.Sleep(2100);
|
||||
|
||||
// Assert
|
||||
Assert.False(limiter.TryAcquire("test_key"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcquireTokens_MultipleKeys()
|
||||
{
|
||||
// Arrange
|
||||
var limiter = new RateLimiter(2, TimeSpan.FromSeconds(1));
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(limiter.TryAcquire("key1"));
|
||||
Assert.True(limiter.TryAcquire("key2"));
|
||||
}
|
||||
}
|
|
@ -292,6 +292,8 @@ public class MangaParserTests
|
|||
[InlineData("Accel World Chapter 001 Volume 002", "1")]
|
||||
[InlineData("Bleach 001-003", "1-3")]
|
||||
[InlineData("Accel World Volume 2", "0")]
|
||||
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
|
||||
[InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")]
|
||||
public void ParseChaptersTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||
|
|
|
@ -180,7 +180,7 @@ Substitute.For<IMediaConversionService>());
|
|||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.Build())
|
||||
.Build())
|
||||
|
@ -246,7 +246,7 @@ Substitute.For<IMediaConversionService>());
|
|||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
|
@ -322,7 +322,7 @@ Substitute.For<IMediaConversionService>());
|
|||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
|
@ -375,7 +375,7 @@ Substitute.For<IMediaConversionService>());
|
|||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
|
@ -428,7 +428,7 @@ Substitute.For<IMediaConversionService>());
|
|||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
|
|
|
@ -395,7 +395,7 @@ public class CleanupServiceTests : AbstractDbTest
|
|||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(c)
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -488,15 +488,21 @@ public class CleanupServiceTests : AbstractDbTest
|
|||
var user = new AppUser()
|
||||
{
|
||||
UserName = "CleanupWantToRead_ShouldRemoveFullyReadSeries",
|
||||
WantToRead = new List<Series>()
|
||||
{
|
||||
s
|
||||
}
|
||||
};
|
||||
_context.AppUser.Add(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Add want to read
|
||||
user.WantToRead = new List<AppUserWantToRead>()
|
||||
{
|
||||
new AppUserWantToRead()
|
||||
{
|
||||
SeriesId = s.Id
|
||||
}
|
||||
};
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
await _readerService.MarkSeriesAsRead(user, s.Id);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -166,7 +166,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -205,7 +205,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -260,7 +260,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -299,7 +299,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -347,19 +347,19 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithNumber(3)
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -382,6 +382,40 @@ public class ReaderServiceTests
|
|||
Assert.Equal("2", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldGetNextVolume_WhenUsingRanges()
|
||||
{
|
||||
// V1 -> V2
|
||||
await ResetDb();
|
||||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1-2")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3-4")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("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();
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("3-4", actualChapter.Volume.Name);
|
||||
Assert.Equal("1", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldGetNextVolume_OnlyFloats()
|
||||
{
|
||||
|
@ -432,19 +466,19 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithNumber(3)
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -473,19 +507,19 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1.5")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithNumber(3)
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -515,13 +549,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
@ -550,18 +584,18 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("66").Build())
|
||||
.WithChapter(new ChapterBuilder("67").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("0").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -592,13 +626,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
|
@ -624,7 +658,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
@ -650,7 +684,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
@ -669,6 +703,37 @@ public class ReaderServiceTests
|
|||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
||||
// This is commented out because, while valid, I can't solve how to make this pass (https://github.com/Kareadita/Kavita/issues/2099)
|
||||
// [Fact]
|
||||
// public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume()
|
||||
// {
|
||||
// await ResetDb();
|
||||
//
|
||||
// var series = new SeriesBuilder("Test")
|
||||
// .WithVolume(new VolumeBuilder("0")
|
||||
// .WithMinNumber(0)
|
||||
// .WithChapter(new ChapterBuilder("1").Build())
|
||||
// .WithChapter(new ChapterBuilder("2").Build())
|
||||
// .Build())
|
||||
// .WithVolume(new VolumeBuilder("1")
|
||||
// .WithMinNumber(1)
|
||||
// .WithChapter(new ChapterBuilder("0").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, 3, 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()
|
||||
|
@ -677,14 +742,14 @@ public class ReaderServiceTests
|
|||
//
|
||||
// var series = new SeriesBuilder("Test")
|
||||
// .WithVolume(new VolumeBuilder("0")
|
||||
// .WithNumber(0)
|
||||
// .WithMinNumber(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)
|
||||
// .WithMinNumber(1)
|
||||
// .WithChapter(new ChapterBuilder("2").Build())
|
||||
// .Build())
|
||||
// .Build();
|
||||
|
@ -711,13 +776,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
|
@ -747,7 +812,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
|
@ -778,13 +843,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -811,12 +876,12 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
|
@ -846,12 +911,12 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("12").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("12").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -872,7 +937,7 @@ public class ReaderServiceTests
|
|||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes);
|
||||
Assert.Equal(2, actualChapter.Volume.Number);
|
||||
Assert.Equal(2, actualChapter.Volume.MinNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -887,19 +952,19 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithNumber(3)
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -930,19 +995,19 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1.5")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithNumber(3)
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -1054,13 +1119,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
|
@ -1092,7 +1157,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
@ -1122,7 +1187,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -1151,13 +1216,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -1186,20 +1251,20 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("5").Build())
|
||||
.WithChapter(new ChapterBuilder("6").Build())
|
||||
.WithChapter(new ChapterBuilder("7").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
|
@ -1234,7 +1299,7 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
@ -1264,12 +1329,12 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
|
@ -1302,12 +1367,12 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(0)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
@ -1340,12 +1405,12 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("12").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithNumber(2)
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("12").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -1361,7 +1426,7 @@ public class ReaderServiceTests
|
|||
|
||||
var nextChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes);
|
||||
Assert.Equal(1, actualChapter.Volume.Number);
|
||||
Assert.Equal(1, actualChapter.Volume.MinNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -1622,6 +1687,35 @@ public class ReaderServiceTests
|
|||
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenHasSpecial()
|
||||
{
|
||||
await ResetDb();
|
||||
var series = new SeriesBuilder("Test")
|
||||
// Loose chapters
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("2").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("Prologue").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();
|
||||
|
||||
var nextChapter = await _readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("1", nextChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstSpecial()
|
||||
{
|
||||
|
|
|
@ -759,7 +759,7 @@ public class ReadingListServiceTests
|
|||
var fablesSeries = new SeriesBuilder("Fables").Build();
|
||||
fablesSeries.Volumes.Add(
|
||||
new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithName("2002")
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.Build()
|
||||
|
@ -937,7 +937,7 @@ public class ReadingListServiceTests
|
|||
var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build();
|
||||
|
||||
fablesSeries.Volumes.Add(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithName("2002")
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
|
@ -945,7 +945,7 @@ public class ReadingListServiceTests
|
|||
.Build()
|
||||
);
|
||||
fables2Series.Volumes.Add(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithName("2003")
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
|
@ -980,13 +980,13 @@ public class ReadingListServiceTests
|
|||
var fables2Series = new SeriesBuilder("Fablesa: The Last Castle").Build();
|
||||
|
||||
fablesSeries.Volumes.Add(new VolumeBuilder("2002")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.Build());
|
||||
fables2Series.Volumes.Add(new VolumeBuilder("2003")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
|
@ -1036,7 +1036,7 @@ public class ReadingListServiceTests
|
|||
// Mock up our series
|
||||
var fablesSeries = new SeriesBuilder("Fables")
|
||||
.WithVolume(new VolumeBuilder("2002")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
|
@ -1045,7 +1045,7 @@ public class ReadingListServiceTests
|
|||
|
||||
var fables2Series = new SeriesBuilder("Fables: The Last Castle")
|
||||
.WithVolume(new VolumeBuilder("2003")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
|
@ -1094,13 +1094,13 @@ public class ReadingListServiceTests
|
|||
var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build();
|
||||
|
||||
fablesSeries.Volumes.Add(new VolumeBuilder("2002")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.Build());
|
||||
fables2Series.Volumes.Add(new VolumeBuilder("2003")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
|
@ -1153,13 +1153,13 @@ public class ReadingListServiceTests
|
|||
var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build();
|
||||
|
||||
fablesSeries.Volumes.Add(new VolumeBuilder("2002")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.Build());
|
||||
fables2Series.Volumes.Add(new VolumeBuilder("2003")
|
||||
.WithNumber(1)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
|
|
|
@ -18,6 +18,7 @@ using API.Services;
|
|||
using API.Services.Plus;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using EasyCaching.Core;
|
||||
using Hangfire;
|
||||
using Hangfire.InMemory;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
@ -1391,4 +1392,96 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeleteMultipleSeries
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteMultipleSeries_ShouldDeleteSeries()
|
||||
{
|
||||
await ResetDb();
|
||||
var lib1 = new LibraryBuilder("Test LIb")
|
||||
.WithSeries(new SeriesBuilder("Test Series")
|
||||
.WithMetadata(new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Everyone
|
||||
})
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("1").WithFile(
|
||||
new MangaFileBuilder($"{DataDirectory}1.zip", MangaFormat.Archive)
|
||||
.WithPages(1)
|
||||
.Build()
|
||||
).Build())
|
||||
.Build())
|
||||
.Build())
|
||||
.WithSeries(new SeriesBuilder("Test Series Prequels").Build())
|
||||
.WithSeries(new SeriesBuilder("Test Series Sequels").Build())
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
|
||||
.Build();
|
||||
_context.Library.Add(lib1);
|
||||
|
||||
var lib2 = new LibraryBuilder("Test LIb 2", LibraryType.Book)
|
||||
.WithSeries(new SeriesBuilder("Test Series 2").Build())
|
||||
.WithSeries(new SeriesBuilder("Test Series Prequels 2").Build())
|
||||
.WithSeries(new SeriesBuilder("Test Series Prequels 2").Build())// TODO: Is this a bug
|
||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
|
||||
.Build();
|
||||
_context.Library.Add(lib2);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1,
|
||||
SeriesIncludes.Related | SeriesIncludes.ExternalRatings);
|
||||
// Add relations
|
||||
var addRelationDto = CreateRelationsDto(series1);
|
||||
addRelationDto.Adaptations.Add(4); // cross library link
|
||||
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||
|
||||
|
||||
// Setup External Metadata stuff
|
||||
series1.ExternalSeriesMetadata ??= new ExternalSeriesMetadata();
|
||||
series1.ExternalSeriesMetadata.ExternalRatings = new List<ExternalRating>()
|
||||
{
|
||||
new ExternalRating()
|
||||
{
|
||||
SeriesId = 1,
|
||||
Provider = ScrobbleProvider.Mal,
|
||||
AverageScore = 1
|
||||
}
|
||||
};
|
||||
series1.ExternalSeriesMetadata.ExternalRecommendations = new List<ExternalRecommendation>()
|
||||
{
|
||||
new ExternalRecommendation()
|
||||
{
|
||||
SeriesId = 2,
|
||||
Name = "Series 2",
|
||||
Url = "",
|
||||
CoverUrl = ""
|
||||
},
|
||||
new ExternalRecommendation()
|
||||
{
|
||||
SeriesId = 0, // Causes a FK constraint
|
||||
Name = "Series 2",
|
||||
Url = "",
|
||||
CoverUrl = ""
|
||||
}
|
||||
};
|
||||
series1.ExternalSeriesMetadata.ExternalReviews = new List<ExternalReview>()
|
||||
{
|
||||
new ExternalReview()
|
||||
{
|
||||
Body = "",
|
||||
Provider = ScrobbleProvider.Mal,
|
||||
BodyJustText = ""
|
||||
}
|
||||
};
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Ensure we can delete the series
|
||||
Assert.True(await _seriesService.DeleteMultipleSeries(new[] {1, 2}));
|
||||
Assert.Null(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1));
|
||||
Assert.Null(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
@ -53,7 +53,9 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
||||
<PackageReference Include="CsvHelper" Version="30.1.0" />
|
||||
<PackageReference Include="MailKit" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -63,28 +65,27 @@
|
|||
<PackageReference Include="ExCSS" Version="4.2.4" />
|
||||
<PackageReference Include="Flurl" Version="3.0.7" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Hangfire" Version="1.8.6" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.6.0" />
|
||||
<PackageReference Include="Hangfire" Version="1.8.9" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.7.0" />
|
||||
<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.4" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.58" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||
<PackageReference Include="NetVips" Version="2.4.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.15.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.7" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.15.1" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.2.0" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||
|
@ -92,17 +93,17 @@
|
|||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.34.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.1" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.15.0.81779">
|
||||
<PackageReference Include="SharpCompress" Version="0.36.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.19.0.84025">
|
||||
<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.12" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="20.0.4" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="20.0.15" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.1" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -190,6 +191,9 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Folder Include="config\themes" />
|
||||
<Content Include="EmailTemplates\**">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Include="I18N\**" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -16,11 +16,7 @@ public static class EasyCacheProfiles
|
|||
/// </summary>
|
||||
public const string Library = "library";
|
||||
/// <summary>
|
||||
/// Metadata filter
|
||||
/// External Series metadata for Kavita+ recommendation
|
||||
/// </summary>
|
||||
public const string Filter = "filter";
|
||||
public const string KavitaPlusReviews = "kavita+reviews";
|
||||
public const string KavitaPlusRecommendations = "kavita+recommendations";
|
||||
public const string KavitaPlusRatings = "kavita+ratings";
|
||||
public const string KavitaPlusExternalSeries = "kavita+externalSeries";
|
||||
}
|
||||
|
|
|
@ -35,7 +35,13 @@ public static class PolicyConstants
|
|||
/// Used to give a user ability to Login to their account
|
||||
/// </summary>
|
||||
public const string LoginRole = "Login";
|
||||
/// <summary>
|
||||
/// Restricts the ability to manage their account without an admin
|
||||
/// </summary>
|
||||
/// <remarks>This is used explicitly for Demo Server. Not sure why it would be used in another fashion</remarks>
|
||||
public const string ReadOnlyRole = "Read Only";
|
||||
|
||||
|
||||
public static readonly ImmutableArray<string> ValidRoles =
|
||||
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole);
|
||||
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole, ReadOnlyRole);
|
||||
}
|
||||
|
|
|
@ -77,10 +77,11 @@ public class AccountController : BaseApiController
|
|||
[HttpPost("reset-password")]
|
||||
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
||||
{
|
||||
_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
|
||||
_logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName);
|
||||
if (User.IsInRole(PolicyConstants.ReadOnlyRole))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||
|
||||
if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin))
|
||||
|
@ -187,12 +188,14 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
user = await _userManager.Users
|
||||
.Include(u => u.UserPreferences)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(x => x.ApiKey == loginDto.ApiKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
user = await _userManager.Users
|
||||
.Include(u => u.UserPreferences)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpperInvariant());
|
||||
}
|
||||
|
||||
|
@ -319,6 +322,7 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<string>> ResetApiKey()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()) ?? throw new KavitaUnauthenticatedUserException();
|
||||
if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
user.ApiKey = HashUtil.ApiKey();
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
|
@ -334,7 +338,9 @@ public class AccountController : BaseApiController
|
|||
|
||||
|
||||
/// <summary>
|
||||
/// Initiates the flow to update a user's email address. The email address is not changed in this API. A confirmation link is sent/dumped which will
|
||||
/// Initiates the flow to update a user's email address.
|
||||
///
|
||||
/// If email is not setup, then the email address is not changed in this API. A confirmation link is sent/dumped which will
|
||||
/// validate the email. It must be confirmed for the email to update.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
|
@ -343,7 +349,7 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
if (user == null || User.IsInRole(PolicyConstants.ReadOnlyRole)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
||||
|
@ -374,14 +380,26 @@ public class AccountController : BaseApiController
|
|||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token"));
|
||||
}
|
||||
|
||||
user.EmailConfirmed = false;
|
||||
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email);
|
||||
user.EmailConfirmed = !shouldEmailUser;
|
||||
user.ConfirmationToken = token;
|
||||
await _userManager.UpdateAsync(user);
|
||||
|
||||
if (!shouldEmailUser)
|
||||
{
|
||||
return Ok(new InviteUserResponse
|
||||
{
|
||||
EmailLink = string.Empty,
|
||||
EmailSent = false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Send a confirmation email
|
||||
try
|
||||
{
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email);
|
||||
var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email);
|
||||
_logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
|
||||
if (!_emailService.IsValidEmail(user.Email))
|
||||
|
@ -396,30 +414,27 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
|
||||
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||
if (accessible)
|
||||
try
|
||||
{
|
||||
try
|
||||
var invitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!;
|
||||
// Email the old address of the update change
|
||||
BackgroundJob.Enqueue(() => _emailService.SendEmailChangeEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
// Email the old address of the update change
|
||||
await _emailService.SendEmailChangeEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email,
|
||||
InstallId = BuildInfo.Version.ToString(),
|
||||
InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!,
|
||||
ServerConfirmationLink = emailLink
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow exception */
|
||||
}
|
||||
EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email,
|
||||
InstallId = BuildInfo.Version.ToString(),
|
||||
InvitingUser = invitingUser,
|
||||
ServerConfirmationLink = emailLink
|
||||
}));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow exception */
|
||||
}
|
||||
|
||||
return Ok(new InviteUserResponse
|
||||
{
|
||||
EmailLink = string.Empty,
|
||||
EmailSent = accessible
|
||||
EmailSent = true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -439,7 +454,7 @@ public class AccountController : BaseApiController
|
|||
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(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
if (!await _accountService.CanChangeAgeRestriction(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
|
||||
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
|
||||
|
@ -574,13 +589,12 @@ public class AccountController : BaseApiController
|
|||
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "manual-setup-fail"));
|
||||
|
||||
return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl);
|
||||
return await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Invites a user to the server. Will generate a setup link for continuing setup. If the server is not accessible, no
|
||||
/// email will be sent.
|
||||
/// Invites a user to the server. Will generate a setup link for continuing setup. If email is not setup, a link will be presented to user to continue setup.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
|
@ -679,15 +693,15 @@ public class AccountController : BaseApiController
|
|||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email);
|
||||
var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email);
|
||||
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
|
||||
if (!_emailService.IsValidEmail(dto.Email))
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
if (!_emailService.IsValidEmail(dto.Email) || !settings.IsEmailSetup())
|
||||
{
|
||||
_logger.LogInformation("[Invite User] {Email} doesn't appear to be an email, so will not send an email to address", dto.Email.Replace(Environment.NewLine, string.Empty));
|
||||
_logger.LogInformation("[Invite User] {Email} doesn't appear to be an email or email is not setup", dto.Email.Replace(Environment.NewLine, string.Empty));
|
||||
return Ok(new InviteUserResponse
|
||||
{
|
||||
EmailLink = emailLink,
|
||||
|
@ -696,22 +710,17 @@ public class AccountController : BaseApiController
|
|||
});
|
||||
}
|
||||
|
||||
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||
if (accessible)
|
||||
BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
// Do the email send on a background thread to ensure UI can move forward without having to wait for a timeout when users use fake emails
|
||||
BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
EmailAddress = dto.Email,
|
||||
InvitingUser = adminUser.UserName,
|
||||
ServerConfirmationLink = emailLink
|
||||
}));
|
||||
}
|
||||
EmailAddress = dto.Email,
|
||||
InvitingUser = adminUser.UserName,
|
||||
ServerConfirmationLink = emailLink
|
||||
}));
|
||||
|
||||
return Ok(new InviteUserResponse
|
||||
{
|
||||
EmailLink = emailLink,
|
||||
EmailSent = accessible
|
||||
EmailSent = true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -837,7 +846,6 @@ public class AccountController : BaseApiController
|
|||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate,
|
||||
MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
|
||||
// Perform Login code
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -882,6 +890,10 @@ public class AccountController : BaseApiController
|
|||
[EnableRateLimiting("Authentication")]
|
||||
public async Task<ActionResult<string>> ForgotPassword([FromQuery] string email)
|
||||
{
|
||||
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
if (!settings.IsEmailSetup()) return Ok(await _localizationService.Get("en", "email-not-enabled"));
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
|
@ -890,32 +902,34 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
|
||||
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole or PolicyConstants.ReadOnlyRole))
|
||||
return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied"));
|
||||
|
||||
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
|
||||
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);
|
||||
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
if (!_emailService.IsValidEmail(user.Email))
|
||||
{
|
||||
_logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send", user.Email);
|
||||
_logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send. Admin must change it in UI", user.Email);
|
||||
return Ok(await _localizationService.Translate(user.Id, "invalid-email"));
|
||||
}
|
||||
if (await _accountService.CheckIfAccessible(Request))
|
||||
{
|
||||
await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto()
|
||||
{
|
||||
EmailAddress = user.Email,
|
||||
ServerConfirmationLink = emailLink,
|
||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||
});
|
||||
return Ok(await _localizationService.Translate(user.Id, "email-sent"));
|
||||
}
|
||||
|
||||
return Ok(await _localizationService.Translate(user.Id, "not-accessible-password"));
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
|
||||
user.ConfirmationToken = token;
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
|
||||
var installId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value;
|
||||
BackgroundJob.Enqueue(() => _emailService.SendForgotPasswordEmail(new PasswordResetEmailDto()
|
||||
{
|
||||
EmailAddress = user.Email,
|
||||
ServerConfirmationLink = emailLink,
|
||||
InstallId = installId
|
||||
}));
|
||||
|
||||
return Ok(await _localizationService.Translate(user.Id, "email-sent"));
|
||||
}
|
||||
|
||||
[HttpGet("email-confirmed")]
|
||||
|
@ -963,9 +977,10 @@ public class AccountController : BaseApiController
|
|||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("resend-confirmation-email")]
|
||||
[EnableRateLimiting("Authentication")]
|
||||
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||
public async Task<ActionResult<InviteUserResponse>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return BadRequest(await _localizationService.Get("en", "no-user"));
|
||||
|
@ -976,96 +991,47 @@ public class AccountController : BaseApiController
|
|||
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);
|
||||
user.ConfirmationToken = token;
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-email-update", user.Email);
|
||||
_logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
|
||||
if (!_emailService.IsValidEmail(user.Email))
|
||||
{
|
||||
_logger.LogCritical("[Email Migration]: User {UserName} is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.UserName, user.Email);
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "invalid-email"));
|
||||
}
|
||||
|
||||
if (await _accountService.CheckIfAccessible(Request))
|
||||
|
||||
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email);
|
||||
|
||||
if (!shouldEmailUser)
|
||||
{
|
||||
try
|
||||
return Ok(new InviteUserResponse()
|
||||
{
|
||||
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
||||
{
|
||||
EmailAddress = user.Email!,
|
||||
Username = user.UserName!,
|
||||
ServerConfirmationLink = emailLink,
|
||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue resending invite email");
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "generic-invite-email"));
|
||||
}
|
||||
return Ok(emailLink);
|
||||
EmailLink = emailLink,
|
||||
EmailSent = false,
|
||||
InvalidEmail = !_emailService.IsValidEmail(user.Email)
|
||||
});
|
||||
}
|
||||
|
||||
return BadRequest(await _localizationService.Translate(user.Id, "not-accessible"));
|
||||
BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
EmailAddress = user.Email!,
|
||||
InvitingUser = User.GetUsername(),
|
||||
ServerConfirmationLink = emailLink,
|
||||
InstallId = serverSettings.InstallId
|
||||
}));
|
||||
|
||||
return Ok(new InviteUserResponse()
|
||||
{
|
||||
EmailLink = emailLink,
|
||||
EmailSent = true,
|
||||
InvalidEmail = !_emailService.IsValidEmail(user.Email)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is similar to invite. Essentially we authenticate the user's password then go through invite email flow
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("migrate-email")]
|
||||
public async Task<ActionResult<string>> MigrateEmail(MigrateUserEmailDto dto)
|
||||
{
|
||||
// If there is an admin account already, return
|
||||
var users = await _unitOfWork.UserRepository.GetAdminUsersAsync();
|
||||
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);
|
||||
if (emailValidationErrors.Any())
|
||||
{
|
||||
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||
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(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(await _localizationService.Get("en", "invalid-username"));
|
||||
|
||||
var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password);
|
||||
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(await _localizationService.Get("en", "critical-email-migration"));
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue during email migration. Contact support");
|
||||
_unitOfWork.UserRepository.Delete(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
@ -92,18 +92,28 @@ public class DeviceController : BaseApiController
|
|||
return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a collection of chapters to the user's device
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("send-to")]
|
||||
public async Task<ActionResult> SendToDevice(SendToDeviceDto dto)
|
||||
{
|
||||
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())
|
||||
var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice();
|
||||
if (!isEmailSetup)
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
|
||||
|
||||
// // Validate that the device belongs to the user
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Devices);
|
||||
if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-unallowed"));
|
||||
|
||||
var userId = User.GetUserId();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"),
|
||||
"started"), userId);
|
||||
try
|
||||
{
|
||||
|
@ -112,16 +122,16 @@ public class DeviceController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
return BadRequest(await _localizationService.Translate(userId, ex.Message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"),
|
||||
"ended"), userId);
|
||||
}
|
||||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
|
||||
return BadRequest(await _localizationService.Translate(userId, "generic-send-to"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -132,7 +142,8 @@ public class DeviceController : BaseApiController
|
|||
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())
|
||||
var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice();
|
||||
if (!isEmailSetup)
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
|
||||
|
||||
var userId = User.GetUserId();
|
||||
|
@ -156,7 +167,7 @@ public class DeviceController : BaseApiController
|
|||
}
|
||||
finally
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"ended"), userId);
|
||||
}
|
||||
|
@ -164,8 +175,6 @@ public class DeviceController : BaseApiController
|
|||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ public class DownloadController : BaseApiController
|
|||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Number}.zip");
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Name}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
|
@ -118,7 +118,7 @@ public class DownloadController : BaseApiController
|
|||
return await _accountService.HasDownloadPermission(user);
|
||||
}
|
||||
|
||||
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
|
||||
private PhysicalFileResult GetFirstFileDownload(IEnumerable<MangaFile> files)
|
||||
{
|
||||
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
|
||||
return PhysicalFile(zipFile, contentType, Uri.EscapeDataString(fileDownloadName), true);
|
||||
|
@ -150,31 +150,40 @@ public class DownloadController : BaseApiController
|
|||
|
||||
private async Task<ActionResult> DownloadFiles(ICollection<MangaFile> files, string tempFolder, string downloadName)
|
||||
{
|
||||
var username = User.GetUsername();
|
||||
var filename = Path.GetFileNameWithoutExtension(downloadName);
|
||||
try
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 0F, "started"));
|
||||
MessageFactory.DownloadProgressEvent(username,
|
||||
filename, $"Downloading {filename}", 0F, "started"));
|
||||
if (files.Count == 1)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
MessageFactory.DownloadProgressEvent(username,
|
||||
filename, $"Downloading {filename}",1F, "ended"));
|
||||
return GetFirstFileDownload(files);
|
||||
}
|
||||
|
||||
var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder);
|
||||
var filePath = _archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
MessageFactory.DownloadProgressEvent(username,
|
||||
filename, "Download Complete", 1F, "ended"));
|
||||
return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true);
|
||||
|
||||
async Task ProgressCallback(Tuple<string, float> progressInfo)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(username, filename, $"Extracting {Path.GetFileNameWithoutExtension(progressInfo.Item1)}",
|
||||
Math.Clamp(progressInfo.Item2, 0F, 1F)));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when trying to download files");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
||||
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
||||
filename, "Download Complete", 1F, "ended"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
@ -216,15 +225,15 @@ public class DownloadController : BaseApiController
|
|||
|
||||
var filename = $"{series!.Name} - Bookmarks.zip";
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 0F));
|
||||
MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}",0F));
|
||||
var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct());
|
||||
var filePath = _archiveService.CreateZipForDownload(files,
|
||||
$"download_{userId}_{seriesIds}_bookmarks");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 1F));
|
||||
MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}", 1F));
|
||||
|
||||
|
||||
return PhysicalFile(filePath, DefaultContentType, System.Web.HttpUtility.UrlEncode(filename), true);
|
||||
return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(filename), true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -112,6 +112,13 @@ public class LibraryController : BaseApiController
|
|||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
|
||||
|
||||
// Restart Folder watching if on
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
if (settings.EnableFolderWatching)
|
||||
{
|
||||
await _libraryWatcher.RestartWatching();
|
||||
}
|
||||
|
||||
// Assign all the necessary users with this library side nav
|
||||
var userIds = admins.Select(u => u.Id).Append(User.GetUserId()).ToList();
|
||||
var userNeedingNewLibrary = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams))
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
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;
|
||||
|
@ -20,7 +19,8 @@ public class LicenseController(
|
|||
IUnitOfWork unitOfWork,
|
||||
ILogger<LicenseController> logger,
|
||||
ILicenseService licenseService,
|
||||
ILocalizationService localizationService)
|
||||
ILocalizationService localizationService,
|
||||
ITaskScheduler taskScheduler)
|
||||
: BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -31,7 +31,9 @@ public class LicenseController(
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)]
|
||||
public async Task<ActionResult<bool>> HasValidLicense(bool forceCheck = false)
|
||||
{
|
||||
return Ok(await licenseService.HasActiveLicense(forceCheck));
|
||||
var result = await licenseService.HasActiveLicense(forceCheck);
|
||||
await taskScheduler.ScheduleKavitaPlusTasks();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -57,6 +59,7 @@ public class LicenseController(
|
|||
setting.Value = null;
|
||||
unitOfWork.SettingsRepository.Update(setting);
|
||||
await unitOfWork.CommitAsync();
|
||||
await taskScheduler.ScheduleKavitaPlusTasks();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -65,7 +68,11 @@ public class LicenseController(
|
|||
public async Task<ActionResult> ResetLicense(UpdateLicenseDto dto)
|
||||
{
|
||||
logger.LogInformation("Resetting license on file for Server");
|
||||
if (await licenseService.ResetLicense(dto.License, dto.Email)) return Ok();
|
||||
if (await licenseService.ResetLicense(dto.License, dto.Email))
|
||||
{
|
||||
await taskScheduler.ScheduleKavitaPlusTasks();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest(localizationService.Translate(User.GetUserId(), "unable-to-reset-k+"));
|
||||
}
|
||||
|
@ -82,6 +89,7 @@ public class LicenseController(
|
|||
try
|
||||
{
|
||||
await licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim(), dto.DiscordId);
|
||||
await taskScheduler.ScheduleKavitaPlusTasks();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -8,9 +8,12 @@ using API.Data;
|
|||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -18,16 +21,11 @@ namespace API.Controllers;
|
|||
|
||||
#nullable enable
|
||||
|
||||
public class MetadataController : BaseApiController
|
||||
public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService,
|
||||
IExternalMetadataService metadataService)
|
||||
: BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
public const string CacheKey = "kavitaPlusSeriesDetail_";
|
||||
|
||||
/// <summary>
|
||||
/// Fetches genres from the instance
|
||||
|
@ -39,12 +37,12 @@ public class MetadataController : BaseApiController
|
|||
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
}
|
||||
|
||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId()));
|
||||
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -53,12 +51,12 @@ public class MetadataController : BaseApiController
|
|||
/// <param name="role">role</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("people-by-role")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"role"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["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()));
|
||||
Ok(await unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role.Value)) :
|
||||
Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -67,15 +65,15 @@ public class MetadataController : BaseApiController
|
|||
/// <param name="libraryIds">String separated libraryIds or null for all people</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("people")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds"])]
|
||||
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
}
|
||||
return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
|
||||
return Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -84,15 +82,15 @@ public class MetadataController : BaseApiController
|
|||
/// <param name="libraryIds">String separated libraryIds or null for all tags</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("tags")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds"])]
|
||||
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId()));
|
||||
}
|
||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId()));
|
||||
return Ok(await unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -101,14 +99,14 @@ public class MetadataController : BaseApiController
|
|||
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
||||
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new [] {"libraryIds"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])]
|
||||
[HttpGet("age-ratings")]
|
||||
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
|
||||
return Ok(await unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
|
||||
}
|
||||
|
||||
return Ok(Enum.GetValues<AgeRating>().Select(t => new AgeRatingDto()
|
||||
|
@ -131,7 +129,7 @@ public class MetadataController : BaseApiController
|
|||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(_unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
|
||||
return Ok(unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
|
||||
}
|
||||
|
||||
return Ok(Enum.GetValues<PublicationStatus>().Select(t => new PublicationStatusDto()
|
||||
|
@ -152,10 +150,13 @@ public class MetadataController : BaseApiController
|
|||
public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
||||
return Ok(await unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns all languages Kavita can accept
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("all-languages")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
|
||||
public IEnumerable<LanguageDto> GetAllValidLanguages()
|
||||
|
@ -177,9 +178,68 @@ public class MetadataController : BaseApiController
|
|||
[HttpGet("chapter-summary")]
|
||||
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
|
||||
{
|
||||
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(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
// TODO: This doesn't seem used anywhere
|
||||
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(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
return Ok(chapter.Summary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this Series is on Kavita+ Blacklist, removes it. If already cached, invalidates it.
|
||||
/// This then attempts to refresh data from Kavita+ for this series.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("force-refresh")]
|
||||
public async Task<ActionResult> ForceRefresh(int seriesId)
|
||||
{
|
||||
await metadataService.ForceKavitaPlusRefresh(seriesId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the details needed from Kavita+ for Series Detail page
|
||||
/// </summary>
|
||||
/// <remarks>This will hit upstream K+ if the data in local db is 2 weeks old</remarks>
|
||||
/// <param name="seriesId">Series Id</param>
|
||||
/// <param name="libraryType">Library Type</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-detail-plus")]
|
||||
public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId, LibraryType libraryType)
|
||||
{
|
||||
var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, User.GetUserId()))
|
||||
.Where(r => !string.IsNullOrEmpty(r.Body))
|
||||
.OrderByDescending(review => review.Username.Equals(User.GetUsername()) ? 1 : 0)
|
||||
.ToList();
|
||||
|
||||
var ret = await metadataService.GetSeriesDetailPlus(seriesId, libraryType);
|
||||
|
||||
await PrepareSeriesDetail(userReviews, ret);
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
private async Task PrepareSeriesDetail(List<UserReviewDto> userReviews, SeriesDetailPlusDto ret)
|
||||
{
|
||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||
var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId())!;
|
||||
|
||||
userReviews.AddRange(ReviewService.SelectSpectrumOfReviews(ret.Reviews.ToList()));
|
||||
ret.Reviews = userReviews;
|
||||
|
||||
if (!isAdmin && ret.Recommendations != null && user != null)
|
||||
{
|
||||
// Re-obtain owned series and take into account age restriction
|
||||
ret.Recommendations.OwnedSeries =
|
||||
await unitOfWork.SeriesRepository.GetSeriesDtoByIdsAsync(
|
||||
ret.Recommendations.OwnedSeries.Select(s => s.Id), user);
|
||||
ret.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
|
||||
}
|
||||
|
||||
if (ret.Recommendations != null && user != null)
|
||||
{
|
||||
ret.Recommendations.OwnedSeries ??= new List<SeriesDto>();
|
||||
await unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, ret.Recommendations.OwnedSeries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -301,7 +301,7 @@ public class OpdsController : BaseApiController
|
|||
/// <returns></returns>
|
||||
[HttpGet("{apiKey}/smart-filter/{filterId}")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSmartFilter(string apiKey, int filterId)
|
||||
public async Task<IActionResult> GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
|
@ -315,7 +315,7 @@ public class OpdsController : BaseApiController
|
|||
SetFeedId(feed, "smartFilter-" + filter.Id);
|
||||
|
||||
var decodedFilter = SmartFilterHelper.Decode(filter.Filter);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, UserParams.Default,
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(pageNumber),
|
||||
decodedFilter);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id));
|
||||
|
||||
|
@ -1109,7 +1109,7 @@ public class OpdsController : BaseApiController
|
|||
title += $" - {volume.Name}";
|
||||
}
|
||||
}
|
||||
else if (volume.Number != 0)
|
||||
else if (volume.MinNumber != 0)
|
||||
{
|
||||
title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
|
||||
}
|
||||
|
@ -1250,7 +1250,7 @@ public class OpdsController : BaseApiController
|
|||
if (progress != null)
|
||||
{
|
||||
link.LastRead = progress.PageNum;
|
||||
link.LastReadDate = progress.LastModifiedUtc;
|
||||
link.LastReadDate = progress.LastModifiedUtc.ToString("s"); // Adhere to ISO 8601
|
||||
}
|
||||
link.IsPageStream = true;
|
||||
return link;
|
||||
|
|
|
@ -57,7 +57,7 @@ public class PanelsController : BaseApiController
|
|||
PageNum = 0,
|
||||
ChapterId = chapterId,
|
||||
VolumeId = 0,
|
||||
SeriesId = 0
|
||||
SeriesId = 0,
|
||||
});
|
||||
return Ok(progress);
|
||||
}
|
||||
|
|
|
@ -14,19 +14,9 @@ namespace API.Controllers;
|
|||
|
||||
#nullable enable
|
||||
|
||||
public class PluginController : BaseApiController
|
||||
public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
|
||||
: BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly ILogger<PluginController> _logger;
|
||||
|
||||
public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tokenService = tokenService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token.
|
||||
/// </summary>
|
||||
|
@ -42,11 +32,11 @@ public class PluginController : BaseApiController
|
|||
// NOTE: In order to log information about plugins, we need some Plugin Description information for each request
|
||||
// Should log into access table so we can tell the user
|
||||
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
var userAgent = HttpContext.Request.Headers["User-Agent"];
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
var userAgent = HttpContext.Request.Headers.UserAgent;
|
||||
var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId <= 0)
|
||||
{
|
||||
_logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", pluginName.Replace(Environment.NewLine, string.Empty), new
|
||||
logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", pluginName.Replace(Environment.NewLine, string.Empty), new
|
||||
{
|
||||
IpAddress = ipAddress,
|
||||
UserAgent = userAgent,
|
||||
|
@ -54,15 +44,15 @@ public class PluginController : BaseApiController
|
|||
});
|
||||
throw new KavitaUnauthenticatedUserException();
|
||||
}
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId);
|
||||
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId);
|
||||
return new UserDto
|
||||
{
|
||||
Username = user.UserName!,
|
||||
Token = await _tokenService.CreateToken(user),
|
||||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
||||
Token = await tokenService.CreateToken(user),
|
||||
RefreshToken = await tokenService.CreateRefreshToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -76,8 +66,8 @@ public class PluginController : BaseApiController
|
|||
[HttpGet("version")]
|
||||
public async Task<ActionResult<string>> GetVersion([Required] string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId <= 0) throw new KavitaUnauthenticatedUserException();
|
||||
return Ok((await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value);
|
||||
return Ok((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,50 +20,12 @@ namespace API.Controllers;
|
|||
/// </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)
|
||||
public RatingController(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")]
|
||||
|
|
|
@ -1,19 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
|
||||
|
@ -22,56 +12,14 @@ 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, IRecommendationService recommendationService,
|
||||
ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory,
|
||||
ILocalizationService localizationService)
|
||||
public RecommendedController(IUnitOfWork unitOfWork)
|
||||
{
|
||||
_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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Quick Reads are series that should be readable in less than 10 in total and are not Ongoing in release.
|
||||
/// </summary>
|
||||
|
@ -79,7 +27,7 @@ public class RecommendedController : BaseApiController
|
|||
/// <param name="userParams">Pagination</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("quick-reads")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams)
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams? userParams)
|
||||
{
|
||||
userParams ??= UserParams.Default;
|
||||
var series = await _unitOfWork.SeriesRepository.GetQuickReads(User.GetUserId(), libraryId, userParams);
|
||||
|
@ -95,7 +43,7 @@ public class RecommendedController : BaseApiController
|
|||
/// <param name="userParams"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("quick-catchup-reads")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams)
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams? userParams)
|
||||
{
|
||||
userParams ??= UserParams.Default;
|
||||
var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(User.GetUserId(), libraryId, userParams);
|
||||
|
@ -111,7 +59,7 @@ public class RecommendedController : BaseApiController
|
|||
/// <param name="userParams">Pagination</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("highly-rated")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams? userParams)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
userParams ??= UserParams.Default;
|
||||
|
@ -129,7 +77,7 @@ public class RecommendedController : BaseApiController
|
|||
/// <param name="userParams">Pagination</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("more-in")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams)
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams? userParams)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
|
@ -148,7 +96,7 @@ public class RecommendedController : BaseApiController
|
|||
/// <param name="userParams">Pagination</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("rediscover")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams userParams)
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams? userParams)
|
||||
{
|
||||
userParams ??= UserParams.Default;
|
||||
var series = await _unitOfWork.SeriesRepository.GetRediscover(User.GetUserId(), libraryId, userParams);
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
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;
|
||||
|
||||
|
@ -22,109 +16,19 @@ 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)
|
||||
public ReviewController(IUnitOfWork unitOfWork,
|
||||
IMapper mapper, IScrobblingService scrobblingService)
|
||||
{
|
||||
_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 username = User.GetUsername();
|
||||
var userRatings = (await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, userId))
|
||||
.Where(r => !string.IsNullOrEmpty(r.Body))
|
||||
.OrderByDescending(review => review.Username.Equals(username) ? 1 : 0)
|
||||
.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 - 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>
|
||||
|
@ -157,4 +61,23 @@ public class ReviewController : BaseApiController
|
|||
_scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body));
|
||||
return Ok(_mapper.Map<UserReviewDto>(rating));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the user's review for the given series
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteReview(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList();
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,22 +39,27 @@ public class ScrobblingController : BaseApiController
|
|||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current user's AniList token
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("anilist-token")]
|
||||
public async Task<ActionResult> GetAniListToken()
|
||||
public async Task<ActionResult<string>> GetAniListToken()
|
||||
{
|
||||
// Validate the license
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
return Ok(user.AniListAccessToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the current user's AniList token
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[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();
|
||||
|
||||
|
@ -71,6 +76,11 @@ public class ScrobblingController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current Scrobbling token for the given Provider has expired for the current user
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("token-expired")]
|
||||
public async Task<ActionResult<bool>> HasTokenExpired(ScrobbleProvider provider)
|
||||
{
|
||||
|
@ -159,15 +169,20 @@ public class ScrobblingController : BaseApiController
|
|||
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"));
|
||||
return Ok(await _localizationService.Translate(user.Id, "nothing-to-do"));
|
||||
|
||||
var seriesHold = new ScrobbleHoldBuilder().WithSeriesId(seriesId).Build();
|
||||
var seriesHold = new ScrobbleHoldBuilder()
|
||||
.WithSeriesId(seriesId)
|
||||
.Build();
|
||||
user.ScrobbleHolds.Add(seriesHold);
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
try
|
||||
{
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// When a hold is placed on a series, clear any pre-existing Scrobble Events
|
||||
await _scrobblingService.ClearEventsForSeries(user.Id, seriesId);
|
||||
return Ok();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException ex)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
|
@ -39,16 +38,14 @@ public class SeriesController : BaseApiController
|
|||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IExternalMetadataService _externalMetadataService;
|
||||
private readonly IEasyCachingProvider _ratingCacheProvider;
|
||||
private readonly IEasyCachingProvider _reviewCacheProvider;
|
||||
private readonly IEasyCachingProvider _recommendationCacheProvider;
|
||||
private readonly IEasyCachingProvider _externalSeriesCacheProvider;
|
||||
private const string CacheKey = "recommendation_";
|
||||
private const string CacheKey = "externalSeriesData_";
|
||||
|
||||
|
||||
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork,
|
||||
ISeriesService seriesService, ILicenseService licenseService,
|
||||
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService, IExternalMetadataService externalMetadataService)
|
||||
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService,
|
||||
IExternalMetadataService externalMetadataService)
|
||||
{
|
||||
_logger = logger;
|
||||
_taskScheduler = taskScheduler;
|
||||
|
@ -58,9 +55,6 @@ public class SeriesController : BaseApiController
|
|||
_localizationService = localizationService;
|
||||
_externalMetadataService = externalMetadataService;
|
||||
|
||||
_ratingCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
|
||||
_reviewCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews);
|
||||
_recommendationCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations);
|
||||
_externalSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries);
|
||||
}
|
||||
|
||||
|
@ -128,6 +122,11 @@ public class SeriesController : BaseApiController
|
|||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a series from Kavita
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns>If the series was deleted or not</returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete("{seriesId}")]
|
||||
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
|
||||
|
@ -145,7 +144,7 @@ public class SeriesController : BaseApiController
|
|||
var username = User.GetUsername();
|
||||
_logger.LogInformation("Series {@SeriesId} is being deleted by {UserName}", dto.SeriesIds, username);
|
||||
|
||||
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();
|
||||
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(true);
|
||||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-delete"));
|
||||
}
|
||||
|
@ -451,19 +450,6 @@ public class SeriesController : BaseApiController
|
|||
if (!await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "update-metadata-fail"));
|
||||
|
||||
if (await _licenseService.HasActiveLicense())
|
||||
{
|
||||
_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 Ok(await _localizationService.Translate(User.GetUserId(), "series-updated"));
|
||||
|
||||
}
|
||||
|
@ -605,7 +591,7 @@ public class SeriesController : BaseApiController
|
|||
await _externalSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(15));
|
||||
return Ok(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
return BadRequest("Unable to load External Series details");
|
||||
}
|
||||
|
|
|
@ -38,18 +38,16 @@ public class ServerController : BaseApiController
|
|||
private readonly IStatsService _statsService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
private readonly IScannerService _scannerService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEasyCachingProviderFactory _cachingProviderFactory;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEmailService _emailService;
|
||||
|
||||
public ServerController(ILogger<ServerController> logger,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||
ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService,
|
||||
IStatsService statsService, ICleanupService cleanupService, IScannerService scannerService,
|
||||
ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory,
|
||||
ILocalizationService localizationService, IEmailService emailService)
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_backupService = backupService;
|
||||
|
@ -58,12 +56,10 @@ public class ServerController : BaseApiController
|
|||
_statsService = statsService;
|
||||
_cleanupService = cleanupService;
|
||||
_scannerService = scannerService;
|
||||
_accountService = accountService;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cachingProviderFactory = cachingProviderFactory;
|
||||
_localizationService = localizationService;
|
||||
_emailService = emailService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -180,15 +176,35 @@ public class ServerController : BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates and pushes an event to the UI
|
||||
/// </summary>
|
||||
/// <remarks>Some users have websocket issues so this is not always reliable to alert the user</remarks>
|
||||
[HttpGet("check-for-updates")]
|
||||
public async Task<ActionResult> CheckForAnnouncements()
|
||||
{
|
||||
await _taskScheduler.CheckForUpdate();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates, if no updates that are > current version installed, returns null
|
||||
/// </summary>
|
||||
[HttpGet("check-update")]
|
||||
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
|
||||
public async Task<ActionResult<UpdateNotificationDto?>> CheckForUpdates()
|
||||
{
|
||||
return Ok(await _versionUpdaterService.CheckForUpdate());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns how many versions out of date this install is
|
||||
/// </summary>
|
||||
[HttpGet("check-out-of-date")]
|
||||
public async Task<ActionResult<int>> CheckHowOutOfDate()
|
||||
{
|
||||
return Ok(await _versionUpdaterService.GetNumberOfReleasesBehind());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Pull the Changelog for Kavita from Github and display
|
||||
|
@ -200,18 +216,6 @@ public class ServerController : BaseApiController
|
|||
return Ok(await _versionUpdaterService.GetAllReleases());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is this server accessible to the outside net
|
||||
/// </summary>
|
||||
/// <remarks>If the instance has the HostName set, this will return true whether or not it is accessible externally</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpGet("accessible")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<bool>> IsServerAccessible()
|
||||
{
|
||||
return Ok(await _accountService.CheckIfAccessible(Request));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of reoccurring jobs. Scheduled ad-hoc jobs will not be returned.
|
||||
/// </summary>
|
||||
|
@ -260,35 +264,13 @@ public class ServerController : BaseApiController
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("bust-review-and-rec-cache")]
|
||||
[HttpPost("bust-kavitaplus-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();
|
||||
provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries);
|
||||
var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries);
|
||||
await provider.FlushAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the KavitaEmail version for non-default instances
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpGet("email-version")]
|
||||
public async Task<ActionResult<string?>> GetEmailVersion()
|
||||
{
|
||||
var emailServiceUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))
|
||||
.Value;
|
||||
|
||||
if (emailServiceUrl.Equals(EmailService.DefaultApiUrl)) return Ok(null);
|
||||
|
||||
return Ok(await _emailService.GetVersion(emailServiceUrl));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||
using API.Data;
|
||||
using API.DTOs.Email;
|
||||
using API.DTOs.Settings;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Converters;
|
||||
|
@ -13,7 +14,8 @@ using API.Logging;
|
|||
using API.Services;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using AutoMapper;
|
||||
using Flurl.Http;
|
||||
using Cronos;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Kavita.Common.Extensions;
|
||||
|
@ -58,6 +60,10 @@ public class SettingsController : BaseApiController
|
|||
return Ok(settingsDto.BaseUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the server settings
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||
|
@ -119,38 +125,15 @@ public class SettingsController : BaseApiController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the email service url
|
||||
/// Is the minimum information setup for Email to work
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset-email-url")]
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetEmailServiceUrlSettings()
|
||||
[HttpGet("is-email-setup")]
|
||||
public async Task<ActionResult<bool>> IsEmailSetup()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is resetting Email Service Url Setting", User.GetUsername());
|
||||
var emailSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl);
|
||||
emailSetting.Value = EmailService.DefaultApiUrl;
|
||||
_unitOfWork.SettingsRepository.Update(emailSetting);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a test email from the Email Service. Will not send if email service is the Default Provider
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("test-email-url")]
|
||||
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
|
||||
var emailService = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
|
||||
return Ok(await _emailService.TestConnectivity(dto.Url, user!.Email, !emailService.Equals(EmailService.DefaultApiUrl)));
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settings.IsEmailSetup());
|
||||
}
|
||||
|
||||
|
||||
|
@ -170,7 +153,8 @@ public class SettingsController : BaseApiController
|
|||
if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") &&
|
||||
!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/"))
|
||||
{
|
||||
bookmarkDirectory = _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
|
||||
bookmarkDirectory =
|
||||
_directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory))
|
||||
|
@ -180,42 +164,29 @@ public class SettingsController : BaseApiController
|
|||
|
||||
foreach (var setting in currentSettings)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskBackup;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
UpdateSchedulingSettings(setting, updateSettingsDto);
|
||||
|
||||
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskScan;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.OnDeckProgressDays && updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value)
|
||||
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)
|
||||
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)
|
||||
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;
|
||||
|
@ -225,7 +196,8 @@ public class SettingsController : BaseApiController
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.CacheSize && updateSettingsDto.CacheSize + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.CacheSize &&
|
||||
updateSettingsDto.CacheSize + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.CacheSize + string.Empty;
|
||||
// CacheSize is managed in appSetting.json
|
||||
|
@ -233,14 +205,21 @@ public class SettingsController : BaseApiController
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
UpdateEmailSettings(setting, updateSettingsDto);
|
||||
|
||||
|
||||
|
||||
if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value)
|
||||
{
|
||||
if (OsInfo.IsDocker) continue;
|
||||
// Validate IP addresses
|
||||
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',',
|
||||
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!IPAddress.TryParse(ipAddress.Trim(), out _)) {
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid", ipAddress));
|
||||
if (!IPAddress.TryParse(ipAddress.Trim(), out _))
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid",
|
||||
ipAddress));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -263,20 +242,23 @@ public class SettingsController : BaseApiController
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.LoggingLevel &&
|
||||
updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
||||
LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.EnableOpds &&
|
||||
updateSettingsDto.EnableOpds + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableOpds + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EncodeMediaAs && updateSettingsDto.EncodeMediaAs + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.EncodeMediaAs &&
|
||||
updateSettingsDto.EncodeMediaAs + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EncodeMediaAs + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
@ -289,23 +271,13 @@ public class SettingsController : BaseApiController
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
||||
setting.Value = UrlHelper.RemoveEndingSlash(setting.Value);
|
||||
FlurlHttp.ConfigureClient(setting.Value, cli =>
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
|
||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
||||
{
|
||||
// Validate new directory can be used
|
||||
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions"));
|
||||
return BadRequest(
|
||||
await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions"));
|
||||
}
|
||||
|
||||
originalBookmarkDirectory = setting.Value;
|
||||
|
@ -316,7 +288,8 @@ public class SettingsController : BaseApiController
|
|||
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.AllowStatCollection &&
|
||||
updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
@ -330,27 +303,32 @@ public class SettingsController : BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.TotalBackups &&
|
||||
updateSettingsDto.TotalBackups + string.Empty != setting.Value)
|
||||
{
|
||||
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-backups"));
|
||||
}
|
||||
|
||||
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TotalLogs && updateSettingsDto.TotalLogs + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.TotalLogs &&
|
||||
updateSettingsDto.TotalLogs + string.Empty != setting.Value)
|
||||
{
|
||||
if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-logs"));
|
||||
}
|
||||
|
||||
setting.Value = updateSettingsDto.TotalLogs + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.EnableFolderWatching &&
|
||||
updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
@ -392,6 +370,97 @@ public class SettingsController : BaseApiController
|
|||
return Ok(updateSettingsDto);
|
||||
}
|
||||
|
||||
private void UpdateSchedulingSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskBackup;
|
||||
_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.TaskCleanup && updateSettingsDto.TaskCleanup != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskCleanup;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.EmailHost &&
|
||||
updateSettingsDto.SmtpConfig.Host + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.Host + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailPort &&
|
||||
updateSettingsDto.SmtpConfig.Port + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.Port + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailAuthPassword &&
|
||||
updateSettingsDto.SmtpConfig.Password + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.Password + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailAuthUserName &&
|
||||
updateSettingsDto.SmtpConfig.UserName + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.UserName + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailSenderAddress &&
|
||||
updateSettingsDto.SmtpConfig.SenderAddress + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.SenderAddress + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailSenderDisplayName &&
|
||||
updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailSizeLimit &&
|
||||
updateSettingsDto.SmtpConfig.SizeLimit + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.SizeLimit + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailEnableSsl &&
|
||||
updateSettingsDto.SmtpConfig.EnableSsl + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.EnableSsl + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailCustomizedTemplates &&
|
||||
updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("task-frequencies")]
|
||||
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
|
||||
|
@ -410,7 +479,7 @@ public class SettingsController : BaseApiController
|
|||
[HttpGet("log-levels")]
|
||||
public ActionResult<IEnumerable<string>> GetLogLevels()
|
||||
{
|
||||
return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"});
|
||||
return Ok(new[] {"Trace", "Debug", "Information", "Warning", "Critical"});
|
||||
}
|
||||
|
||||
[HttpGet("opds-enabled")]
|
||||
|
@ -419,4 +488,28 @@ public class SettingsController : BaseApiController
|
|||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settingsDto.EnableOpds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the cron expression valid for Kavita's scheduler
|
||||
/// </summary>
|
||||
/// <param name="cronExpression"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("is-valid-cron")]
|
||||
public ActionResult<bool> IsValidCron(string cronExpression)
|
||||
{
|
||||
// NOTE: This must match Hangfire's underlying cron system. Hangfire is unique
|
||||
return Ok(CronHelper.IsValidCron(cronExpression));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a test email to see if email settings are hooked up correctly
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("test-email-url")]
|
||||
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
|
||||
return Ok(await _emailService.SendTestEmail(user!.Email));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using API.DTOs;
|
|||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.WantToRead;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
|
@ -91,15 +92,15 @@ public class WantToReadController : BaseApiController
|
|||
AppUserIncludes.WantToRead);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var existingIds = user.WantToRead.Select(s => s.Id).ToList();
|
||||
existingIds.AddRange(dto.SeriesIds);
|
||||
var existingIds = user.WantToRead.Select(s => s.SeriesId).ToList();
|
||||
var idsToAdd = dto.SeriesIds.Except(existingIds);
|
||||
|
||||
var idsToAdd = existingIds.Distinct().ToList();
|
||||
|
||||
var seriesToAdd = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(idsToAdd);
|
||||
foreach (var series in seriesToAdd)
|
||||
foreach (var id in idsToAdd)
|
||||
{
|
||||
user.WantToRead.Add(series);
|
||||
user.WantToRead.Add(new AppUserWantToRead()
|
||||
{
|
||||
SeriesId = id
|
||||
});
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
|
@ -127,7 +128,9 @@ public class WantToReadController : BaseApiController
|
|||
AppUserIncludes.WantToRead);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList();
|
||||
user.WantToRead = user.WantToRead
|
||||
.Where(s => !dto.SeriesIds.Contains(s.SeriesId))
|
||||
.ToList();
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
|
|
|
@ -7,4 +7,5 @@ public class EmailTestResultDto
|
|||
{
|
||||
public bool Successful { get; set; }
|
||||
public string ErrorMessage { get; set; } = default!;
|
||||
public string EmailAddress { get; set; } = default!;
|
||||
}
|
||||
|
|
|
@ -30,4 +30,8 @@ public enum SortField
|
|||
/// Last time the user had any reading progress
|
||||
/// </summary>
|
||||
ReadProgress = 7,
|
||||
/// <summary>
|
||||
/// Kavita+ Only - External Average Rating
|
||||
/// </summary>
|
||||
AverageRating = 8
|
||||
}
|
||||
|
|
|
@ -45,5 +45,9 @@ public enum FilterField
|
|||
/// Last time User Read
|
||||
/// </summary>
|
||||
ReadingDate = 27,
|
||||
/// <summary>
|
||||
/// Average rating from Kavita+ - Not usable for non-licensed users
|
||||
/// </summary>
|
||||
AverageRating = 28
|
||||
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ public class FeedLink
|
|||
/// </summary>
|
||||
/// <remarks>Attribute MUST conform Atom's Date construct</remarks>
|
||||
[XmlAttribute("lastReadDate", Namespace = "http://vaemendis.net/opds-pse/ns")]
|
||||
public DateTime LastReadDate { get; set; }
|
||||
public string LastReadDate { get; set; }
|
||||
|
||||
public bool ShouldSerializeLastReadDate()
|
||||
{
|
||||
|
|
17
API/DTOs/Scrobbling/MediaRecommendationDto.cs
Normal file
17
API/DTOs/Scrobbling/MediaRecommendationDto.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
using API.Services.Plus;
|
||||
|
||||
namespace API.DTOs.Scrobbling;
|
||||
|
||||
public record MediaRecommendationDto
|
||||
{
|
||||
public int Rating { get; set; }
|
||||
public IEnumerable<string> RecommendationNames { get; set; } = null!;
|
||||
public string Name { get; set; }
|
||||
public string CoverUrl { get; set; }
|
||||
public string SiteUrl { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public int? AniListId { get; set; }
|
||||
public long? MalId { get; set; }
|
||||
public ScrobbleProvider Provider { get; set; }
|
||||
}
|
21
API/DTOs/Scrobbling/PlusSeriesDto.cs
Normal file
21
API/DTOs/Scrobbling/PlusSeriesDto.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace API.DTOs.Scrobbling;
|
||||
|
||||
public record PlusSeriesDto
|
||||
{
|
||||
public int? AniListId { get; set; }
|
||||
public long? MalId { get; set; }
|
||||
public string? GoogleBooksId { get; set; }
|
||||
public string? MangaDexId { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string? AltSeriesName { get; set; }
|
||||
public MediaFormat MediaFormat { get; set; }
|
||||
/// <summary>
|
||||
/// Optional but can help with matching
|
||||
/// </summary>
|
||||
public int? ChapterCount { get; set; }
|
||||
/// <summary>
|
||||
/// Optional but can help with matching
|
||||
/// </summary>
|
||||
public int? VolumeCount { get; set; }
|
||||
public int? Year { get; set; }
|
||||
}
|
|
@ -8,11 +8,13 @@ public class ScrobbleEventDto
|
|||
public int SeriesId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public bool IsProcessed { get; set; }
|
||||
public int? VolumeNumber { get; set; }
|
||||
public float? 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; }
|
||||
public bool IsErrored { get; set; }
|
||||
public string? ErrorDetails { get; set; }
|
||||
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ namespace API.DTOs.SeriesDetail;
|
|||
public class NextExpectedChapterDto
|
||||
{
|
||||
public float ChapterNumber { get; set; }
|
||||
public int VolumeNumber { get; set; }
|
||||
public float VolumeNumber { get; set; }
|
||||
/// <summary>
|
||||
/// Null if not applicable
|
||||
/// </summary>
|
||||
|
|
15
API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs
Normal file
15
API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
using API.DTOs.Recommendation;
|
||||
|
||||
namespace API.DTOs.SeriesDetail;
|
||||
|
||||
/// <summary>
|
||||
/// All the data from Kavita+ for Series Detail
|
||||
/// </summary>
|
||||
/// <remarks>This is what the UI sees, not what the API sends back</remarks>
|
||||
public class SeriesDetailPlusDto
|
||||
{
|
||||
public RecommendationDto? Recommendations { get; set; }
|
||||
public IEnumerable<UserReviewDto> Reviews { get; set; }
|
||||
public IEnumerable<RatingDto>? Ratings { get; set; }
|
||||
}
|
|
@ -14,27 +14,29 @@ public class UserReviewDto
|
|||
/// </summary>
|
||||
/// <remarks>This is not possible to set as a local user</remarks>
|
||||
public string? Tagline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The main review
|
||||
/// </summary>
|
||||
public string Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The main body with just text, for review preview
|
||||
/// </summary>
|
||||
public string? BodyJustText { 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; }
|
||||
|
||||
public int TotalVotes { get; set; }
|
||||
public float Rating { get; set; }
|
||||
public string? RawBody { get; set; }
|
||||
/// <summary>
|
||||
/// How many upvotes this review has gotten
|
||||
/// </summary>
|
||||
|
@ -43,16 +45,11 @@ public class UserReviewDto
|
|||
/// <summary>
|
||||
/// If External, the url of the review
|
||||
/// </summary>
|
||||
public string? ExternalUrl { get; set; }
|
||||
public string? SiteUrl { 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>
|
||||
|
|
20
API/DTOs/Settings/SMTPConfigDto.cs
Normal file
20
API/DTOs/Settings/SMTPConfigDto.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
namespace API.DTOs.Settings;
|
||||
|
||||
public class SmtpConfigDto
|
||||
{
|
||||
public string SenderAddress { get; set; } = string.Empty;
|
||||
public string SenderDisplayName { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string Host { get; set; } = string.Empty;
|
||||
public int Port { get; set; } = 0;
|
||||
public bool EnableSsl { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Limit in bytes for allowing files to be added as attachments. Defaults to 25MB
|
||||
/// </summary>
|
||||
public int SizeLimit { get; set; } = 26_214_400;
|
||||
/// <summary>
|
||||
/// Should Kavita use config/templates for Email templates or the default ones
|
||||
/// </summary>
|
||||
public bool CustomizedTemplates { get; set; } = false;
|
||||
}
|
|
@ -8,11 +8,12 @@ public class ServerSettingDto
|
|||
|
||||
public string CacheDirectory { get; set; } = default!;
|
||||
public string TaskScan { get; set; } = default!;
|
||||
public string TaskBackup { get; set; } = default!;
|
||||
public string TaskCleanup { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// Logging level for server. Managed in appsettings.json.
|
||||
/// </summary>
|
||||
public string LoggingLevel { get; set; } = default!;
|
||||
public string TaskBackup { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// Port the server listens on. Managed in appsettings.json.
|
||||
/// </summary>
|
||||
|
@ -38,11 +39,6 @@ public class ServerSettingDto
|
|||
/// </summary>
|
||||
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="DirectoryService.BookmarkDirectory"/></remarks>
|
||||
public string BookmarksDirectory { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// Email service to use for the invite user flow, forgot password, etc.
|
||||
/// </summary>
|
||||
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="EmailService.DefaultApiUrl"/></remarks>
|
||||
public string EmailServiceUrl { get; set; } = default!;
|
||||
public string InstallVersion { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs.
|
||||
|
@ -88,4 +84,29 @@ public class ServerSettingDto
|
|||
/// How large the cover images should be
|
||||
/// </summary>
|
||||
public CoverImageSize CoverImageSize { get; set; }
|
||||
/// <summary>
|
||||
/// SMTP Configuration
|
||||
/// </summary>
|
||||
public SmtpConfigDto SmtpConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Are at least some basics filled in
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsEmailSetup()
|
||||
{
|
||||
return !string.IsNullOrEmpty(SmtpConfig.Host)
|
||||
&& !string.IsNullOrEmpty(SmtpConfig.UserName)
|
||||
&& !string.IsNullOrEmpty(HostName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Are at least some basics filled in, but not hostname as not required for Send to Device
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsEmailSetupForSendToDevice()
|
||||
{
|
||||
return !string.IsNullOrEmpty(SmtpConfig.Host)
|
||||
&& !string.IsNullOrEmpty(SmtpConfig.UserName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,4 +38,16 @@ public class UpdateNotificationDto
|
|||
/// Date of the publish
|
||||
/// </summary>
|
||||
public required string PublishDate { get; init; }
|
||||
/// <summary>
|
||||
/// Is the server on a nightly within this release
|
||||
/// </summary>
|
||||
public bool IsOnNightlyInRelease { get; set; }
|
||||
/// <summary>
|
||||
/// Is the server on an older version
|
||||
/// </summary>
|
||||
public bool IsReleaseNewer { get; set; }
|
||||
/// <summary>
|
||||
/// Is the server on this version
|
||||
/// </summary>
|
||||
public bool IsReleaseEqual { get; set; }
|
||||
}
|
||||
|
|
|
@ -9,11 +9,17 @@ namespace API.DTOs;
|
|||
public class VolumeDto : IHasReadTimeEstimate
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <inheritdoc cref="Volume.Number"/>
|
||||
public int Number { get; set; }
|
||||
|
||||
/// <inheritdoc cref="Volume.MinNumber"/>
|
||||
public float MinNumber { get; set; }
|
||||
/// <inheritdoc cref="Volume.MaxNumber"/>
|
||||
public float MaxNumber { get; set; }
|
||||
/// <inheritdoc cref="Volume.Name"/>
|
||||
public string Name { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14
|
||||
/// </summary>
|
||||
[Obsolete("Use MinNumber")]
|
||||
public float Number { get; set; }
|
||||
public int Pages { get; set; }
|
||||
public int PagesRead { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
|
|
|
@ -58,6 +58,12 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<AppUserDashboardStream> AppUserDashboardStream { get; set; } = null!;
|
||||
public DbSet<AppUserSideNavStream> AppUserSideNavStream { get; set; } = null!;
|
||||
public DbSet<AppUserExternalSource> AppUserExternalSource { get; set; } = null!;
|
||||
public DbSet<ExternalReview> ExternalReview { get; set; } = null!;
|
||||
public DbSet<ExternalRating> ExternalRating { get; set; } = null!;
|
||||
public DbSet<ExternalSeriesMetadata> ExternalSeriesMetadata { get; set; } = null!;
|
||||
public DbSet<ExternalRecommendation> ExternalRecommendation { get; set; } = null!;
|
||||
public DbSet<ManualMigrationHistory> ManualMigrationHistory { get; set; } = null!;
|
||||
public DbSet<SeriesBlacklist> SeriesBlacklist { get; set; } = null!;
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
@ -137,9 +143,15 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
builder.Entity<AppUserSideNavStream>()
|
||||
.HasIndex(e => e.Visible)
|
||||
.IsUnique(false);
|
||||
|
||||
builder.Entity<ExternalSeriesMetadata>()
|
||||
.HasOne(em => em.Series)
|
||||
.WithOne(s => s.ExternalSeriesMetadata)
|
||||
.HasForeignKey<ExternalSeriesMetadata>(em => em.SeriesId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
|
||||
#nullable enable
|
||||
private static void OnEntityTracked(object? sender, EntityTrackedEventArgs e)
|
||||
{
|
||||
if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return;
|
||||
|
@ -156,6 +168,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
entity.LastModified = DateTime.Now;
|
||||
entity.LastModifiedUtc = DateTime.UtcNow;
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
private void OnSaveChanges()
|
||||
{
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// For the v0.7.14 release, one of the nightlies had bad data that would cause issues. This drops those records
|
||||
/// </summary>
|
||||
public static class MigrateClearNightlyExternalSeriesRecords
|
||||
{
|
||||
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateClearNightlyExternalSeriesRecords"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateClearNightlyExternalSeriesRecords migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
dataContext.ExternalSeriesMetadata.RemoveRange(dataContext.ExternalSeriesMetadata);
|
||||
dataContext.ExternalRating.RemoveRange(dataContext.ExternalRating);
|
||||
dataContext.ExternalRecommendation.RemoveRange(dataContext.ExternalRecommendation);
|
||||
dataContext.ExternalReview.RemoveRange(dataContext.ExternalReview);
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateClearNightlyExternalSeriesRecords",
|
||||
ProductVersion = BuildInfo.Version.ToString(),
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateClearNightlyExternalSeriesRecords migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
59
API/Data/ManualMigrations/MigrateEmailTemplates.cs
Normal file
59
API/Data/ManualMigrations/MigrateEmailTemplates.cs
Normal file
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Services;
|
||||
using Flurl.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
public static class MigrateEmailTemplates
|
||||
{
|
||||
private const string EmailChange = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailChange.html";
|
||||
private const string EmailConfirm = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailConfirm.html";
|
||||
private const string EmailPasswordReset = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailPasswordReset.html";
|
||||
private const string SendToDevice = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/SendToDevice.html";
|
||||
private const string EmailTest = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailTest.html";
|
||||
|
||||
public static async Task Migrate(IDirectoryService directoryService, ILogger<Program> logger)
|
||||
{
|
||||
var files = directoryService.GetFiles(directoryService.CustomizedTemplateDirectory);
|
||||
if (files.Any())
|
||||
{
|
||||
logger.LogCritical("Running MigrateEmailTemplates migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Write files to directory
|
||||
await DownloadAndWriteToFile(EmailChange, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailChange.html"), logger);
|
||||
await DownloadAndWriteToFile(EmailConfirm, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailConfirm.html"), logger);
|
||||
await DownloadAndWriteToFile(EmailPasswordReset, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailPasswordReset.html"), logger);
|
||||
await DownloadAndWriteToFile(SendToDevice, Path.Join(directoryService.CustomizedTemplateDirectory, "SendToDevice.html"), logger);
|
||||
await DownloadAndWriteToFile(EmailTest, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailTest.html"), logger);
|
||||
|
||||
|
||||
|
||||
logger.LogCritical("Running MigrateEmailTemplates migration - Please be patient, this may take some time. This is not an error");
|
||||
}
|
||||
|
||||
private static async Task DownloadAndWriteToFile(string url, string filePath, ILogger<Program> logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Download the raw text using Flurl
|
||||
var content = await url.GetStringAsync();
|
||||
|
||||
// Write the content to a file
|
||||
await File.WriteAllTextAsync(filePath, content);
|
||||
|
||||
logger.LogInformation("{File} downloaded and written successfully", filePath);
|
||||
}
|
||||
catch (FlurlHttpException ex)
|
||||
{
|
||||
logger.LogError(ex, "Unable to download {Url} to {FilePath}. Please perform yourself!", url, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -15,9 +15,20 @@ public static class MigrateLibrariesToHaveAllFileTypes
|
|||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
if (await dataContext.Library.AnyAsync(l => l.LibraryFileTypes.Count == 0))
|
||||
{
|
||||
logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Please be patient, this may take some time. This is not an error");
|
||||
var allLibs = await dataContext.Library.Include(l => l.LibraryFileTypes).ToListAsync();
|
||||
foreach (var library in allLibs.Where(library => library.LibraryFileTypes.Count == 0))
|
||||
|
||||
var allLibs = await dataContext.Library
|
||||
.Include(l => l.LibraryFileTypes)
|
||||
.Where(library => library.LibraryFileTypes.Count == 0)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var library in allLibs)
|
||||
{
|
||||
switch (library.Type)
|
||||
{
|
||||
|
@ -57,11 +68,14 @@ public static class MigrateLibrariesToHaveAllFileTypes
|
|||
});
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
if (unitOfWork.HasChanges())
|
||||
{
|
||||
await dataContext.SaveChangesAsync();
|
||||
}
|
||||
logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
||||
|
|
86
API/Data/ManualMigrations/MigrateManualHistory.cs
Normal file
86
API/Data/ManualMigrations/MigrateManualHistory.cs
Normal file
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.7.14, will store history so that going forward, migrations can just check against the history
|
||||
/// and I don't need to remove old migrations
|
||||
/// </summary>
|
||||
public static class MigrateManualHistory
|
||||
{
|
||||
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
if (await dataContext.ManualMigrationHistory.AnyAsync())
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateManualHistory migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateManualHistory migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateUserLibrarySideNavStream",
|
||||
ProductVersion = "0.7.9.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateSmartFilterEncoding",
|
||||
ProductVersion = "0.7.11.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateLibrariesToHaveAllFileTypes",
|
||||
ProductVersion = "0.7.11.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateEmailTemplates",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateVolumeNumber",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateWantToReadExport",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateWantToReadImport",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateManualHistory",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateManualHistory migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
|
@ -14,9 +14,9 @@ public static class MigrateUserLibrarySideNavStream
|
|||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
var usersWithLibraryStreams = await dataContext.AppUser.Include(u => u.SideNavStreams)
|
||||
var usersWithLibraryStreams = await dataContext.AppUser
|
||||
.Include(u => u.SideNavStreams)
|
||||
.AnyAsync(u => u.SideNavStreams.Count > 0 && u.SideNavStreams.Any(s => s.LibraryId > 0));
|
||||
|
||||
if (usersWithLibraryStreams)
|
||||
|
@ -25,6 +25,8 @@ public static class MigrateUserLibrarySideNavStream
|
|||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
var users = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams);
|
||||
foreach (var user in users)
|
||||
{
|
||||
|
|
39
API/Data/ManualMigrations/MigrateVolumeNumber.cs
Normal file
39
API/Data/ManualMigrations/MigrateVolumeNumber.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities.Enums;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.7.14, this migrates the existing Volume Name -> Volume Min/Max Number
|
||||
/// </summary>
|
||||
public static class MigrateVolumeNumber
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0))
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateVolumeNumber migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateVolumeNumber migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
// Get all volumes
|
||||
foreach (var volume in dataContext.Volume)
|
||||
{
|
||||
volume.MinNumber = Parser.MinNumberFromRange(volume.Name);
|
||||
volume.MaxNumber = Parser.MaxNumberFromRange(volume.Name);
|
||||
}
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
logger.LogCritical(
|
||||
"Running MigrateVolumeNumber migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
79
API/Data/ManualMigrations/MigrateWantToReadExport.cs
Normal file
79
API/Data/ManualMigrations/MigrateWantToReadExport.cs
Normal file
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using API.Services;
|
||||
using CsvHelper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// v0.7.13.12/v0.7.14 - Want to read is extracted and saved in a csv
|
||||
/// </summary>
|
||||
/// <remarks>This must run BEFORE any DB migrations</remarks>
|
||||
public static class MigrateWantToReadExport
|
||||
{
|
||||
public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger<Program> logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
|
||||
if (File.Exists(importFile))
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadExport migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadExport migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
await using var command = dataContext.Database.GetDbConnection().CreateCommand();
|
||||
command.CommandText = "Select AppUserId, Id from Series WHERE AppUserId IS NOT NULL ORDER BY AppUserId;";
|
||||
|
||||
await dataContext.Database.OpenConnectionAsync();
|
||||
await using var result = await command.ExecuteReaderAsync();
|
||||
|
||||
await using var writer =
|
||||
new StreamWriter(Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"));
|
||||
await using var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture);
|
||||
|
||||
// Write header
|
||||
csvWriter.WriteField("AppUserId");
|
||||
csvWriter.WriteField("Id");
|
||||
await csvWriter.NextRecordAsync();
|
||||
|
||||
// Write data
|
||||
while (await result.ReadAsync())
|
||||
{
|
||||
var appUserId = result["AppUserId"].ToString();
|
||||
var id = result["Id"].ToString();
|
||||
|
||||
csvWriter.WriteField(appUserId);
|
||||
csvWriter.WriteField(id);
|
||||
await csvWriter.NextRecordAsync();
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await dataContext.Database.CloseConnectionAsync();
|
||||
writer.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow */
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadExport migration - Completed. This is not an error");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// On new installs, the db isn't setup yet, so this has nothing to do
|
||||
}
|
||||
}
|
||||
}
|
60
API/Data/ManualMigrations/MigrateWantToReadImport.cs
Normal file
60
API/Data/ManualMigrations/MigrateWantToReadImport.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Services;
|
||||
using CsvHelper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.7.13.12/v0.7.14 - Want to read is imported from a csv
|
||||
/// </summary>
|
||||
public static class MigrateWantToReadImport
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger<Program> logger)
|
||||
{
|
||||
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
|
||||
var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv");
|
||||
|
||||
if (!File.Exists(importFile) || File.Exists(outputFile))
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadImport migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadImport migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
using var reader = new StreamReader(importFile);
|
||||
using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture);
|
||||
// Read the records from the CSV file
|
||||
await csvReader.ReadAsync();
|
||||
csvReader.ReadHeader(); // Skip the header row
|
||||
|
||||
while (await csvReader.ReadAsync())
|
||||
{
|
||||
// Read the values of AppUserId and Id columns
|
||||
var appUserId = csvReader.GetField<int>("AppUserId");
|
||||
var seriesId = csvReader.GetField<int>("Id");
|
||||
var user = await unitOfWork.UserRepository.GetUserByIdAsync(appUserId, AppUserIncludes.WantToRead);
|
||||
if (user == null || user.WantToRead.Any(w => w.SeriesId == seriesId)) continue;
|
||||
|
||||
user.WantToRead.Add(new AppUserWantToRead()
|
||||
{
|
||||
SeriesId = seriesId
|
||||
});
|
||||
}
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
reader.Close();
|
||||
|
||||
File.WriteAllLines(outputFile, await File.ReadAllLinesAsync(importFile));
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadImport migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
2787
API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs
generated
Normal file
2787
API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
227
API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs
Normal file
227
API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs
Normal file
|
@ -0,0 +1,227 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ExternalSeriesMetadata : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExternalRating",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AverageScore = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
FavoriteCount = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Provider = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ProviderUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExternalRating", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExternalRecommendation",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CoverUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Url = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Summary = table.Column<string>(type: "TEXT", nullable: true),
|
||||
AniListId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
MalId = table.Column<long>(type: "INTEGER", nullable: true),
|
||||
Provider = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExternalRecommendation", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExternalRecommendation_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExternalReview",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Tagline = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Body = table.Column<string>(type: "TEXT", nullable: true),
|
||||
BodyJustText = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RawBody = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Provider = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SiteUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Username = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Rating = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Score = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TotalVotes = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExternalReview", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExternalSeriesMetadata",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
AverageExternalRating = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AniListId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
MalId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
GoogleBooksId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
LastUpdatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExternalSeriesMetadata", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExternalSeriesMetadata_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExternalRatingExternalSeriesMetadata",
|
||||
columns: table => new
|
||||
{
|
||||
ExternalRatingsId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ExternalSeriesMetadatasId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExternalRatingExternalSeriesMetadata", x => new { x.ExternalRatingsId, x.ExternalSeriesMetadatasId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ExternalRatingExternalSeriesMetadata_ExternalRating_ExternalRatingsId",
|
||||
column: x => x.ExternalRatingsId,
|
||||
principalTable: "ExternalRating",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExternalRatingExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId",
|
||||
column: x => x.ExternalSeriesMetadatasId,
|
||||
principalTable: "ExternalSeriesMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExternalRecommendationExternalSeriesMetadata",
|
||||
columns: table => new
|
||||
{
|
||||
ExternalRecommendationsId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ExternalSeriesMetadatasId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExternalRecommendationExternalSeriesMetadata", x => new { x.ExternalRecommendationsId, x.ExternalSeriesMetadatasId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ExternalRecommendationExternalSeriesMetadata_ExternalRecommendation_ExternalRecommendationsId",
|
||||
column: x => x.ExternalRecommendationsId,
|
||||
principalTable: "ExternalRecommendation",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExternalRecommendationExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId",
|
||||
column: x => x.ExternalSeriesMetadatasId,
|
||||
principalTable: "ExternalSeriesMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExternalReviewExternalSeriesMetadata",
|
||||
columns: table => new
|
||||
{
|
||||
ExternalReviewsId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ExternalSeriesMetadatasId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExternalReviewExternalSeriesMetadata", x => new { x.ExternalReviewsId, x.ExternalSeriesMetadatasId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ExternalReviewExternalSeriesMetadata_ExternalReview_ExternalReviewsId",
|
||||
column: x => x.ExternalReviewsId,
|
||||
principalTable: "ExternalReview",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExternalReviewExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId",
|
||||
column: x => x.ExternalSeriesMetadatasId,
|
||||
principalTable: "ExternalSeriesMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalRatingExternalSeriesMetadata_ExternalSeriesMetadatasId",
|
||||
table: "ExternalRatingExternalSeriesMetadata",
|
||||
column: "ExternalSeriesMetadatasId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalRecommendation_SeriesId",
|
||||
table: "ExternalRecommendation",
|
||||
column: "SeriesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalRecommendationExternalSeriesMetadata_ExternalSeriesMetadatasId",
|
||||
table: "ExternalRecommendationExternalSeriesMetadata",
|
||||
column: "ExternalSeriesMetadatasId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalReviewExternalSeriesMetadata_ExternalSeriesMetadatasId",
|
||||
table: "ExternalReviewExternalSeriesMetadata",
|
||||
column: "ExternalSeriesMetadatasId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalSeriesMetadata_SeriesId",
|
||||
table: "ExternalSeriesMetadata",
|
||||
column: "SeriesId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExternalRatingExternalSeriesMetadata");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExternalRecommendationExternalSeriesMetadata");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExternalReviewExternalSeriesMetadata");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExternalRating");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExternalRecommendation");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExternalReview");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExternalSeriesMetadata");
|
||||
}
|
||||
}
|
||||
}
|
2793
API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs
generated
Normal file
2793
API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
40
API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs
Normal file
40
API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class VolumeMinMaxNumbers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "MaxNumber",
|
||||
table: "Volume",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f);
|
||||
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "MinNumber",
|
||||
table: "Volume",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxNumber",
|
||||
table: "Volume");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinNumber",
|
||||
table: "Volume");
|
||||
}
|
||||
}
|
||||
}
|
2844
API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs
generated
Normal file
2844
API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
106
API/Data/Migrations/20240130190617_WantToReadFix.cs
Normal file
106
API/Data/Migrations/20240130190617_WantToReadFix.cs
Normal file
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class WantToReadFix : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Series_AspNetUsers_AppUserId",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Series_AppUserId",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AppUserId",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserWantToRead",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AppUserWantToRead", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserWantToRead_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserWantToRead_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ManualMigrationHistory",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ProductVersion = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RanAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ManualMigrationHistory", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserWantToRead_AppUserId",
|
||||
table: "AppUserWantToRead",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserWantToRead_SeriesId",
|
||||
table: "AppUserWantToRead",
|
||||
column: "SeriesId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserWantToRead");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ManualMigrationHistory");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AppUserId",
|
||||
table: "Series",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Series_AppUserId",
|
||||
table: "Series",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Series_AspNetUsers_AppUserId",
|
||||
table: "Series",
|
||||
column: "AppUserId",
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
2874
API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs
generated
Normal file
2874
API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
57
API/Data/Migrations/20240204141206_BlackListSeries.cs
Normal file
57
API/Data/Migrations/20240204141206_BlackListSeries.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class BlackListSeries : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "LastUpdatedUtc",
|
||||
table: "ExternalSeriesMetadata",
|
||||
newName: "ValidUntilUtc");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesBlacklist",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
LastChecked = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesBlacklist", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesBlacklist_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesBlacklist_SeriesId",
|
||||
table: "SeriesBlacklist",
|
||||
column: "SeriesId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesBlacklist");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ValidUntilUtc",
|
||||
table: "ExternalSeriesMetadata",
|
||||
newName: "LastUpdatedUtc");
|
||||
}
|
||||
}
|
||||
}
|
2880
API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs
generated
Normal file
2880
API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
57
API/Data/Migrations/20240205184724_ScrobbleEventError.cs
Normal file
57
API/Data/Migrations/20240205184724_ScrobbleEventError.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ScrobbleEventError : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "VolumeNumber",
|
||||
table: "ScrobbleEvent",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ErrorDetails",
|
||||
table: "ScrobbleEvent",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsErrored",
|
||||
table: "ScrobbleEvent",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ErrorDetails",
|
||||
table: "ScrobbleEvent");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsErrored",
|
||||
table: "ScrobbleEvent");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "VolumeNumber",
|
||||
table: "ScrobbleEvent",
|
||||
type: "INTEGER",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
2871
API/Data/Migrations/20240209224347_DBTweaks.Designer.cs
generated
Normal file
2871
API/Data/Migrations/20240209224347_DBTweaks.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
29
API/Data/Migrations/20240209224347_DBTweaks.cs
Normal file
29
API/Data/Migrations/20240209224347_DBTweaks.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class DBTweaks : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ExternalRecommendation_Series_SeriesId",
|
||||
table: "ExternalRecommendation");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ExternalRecommendation_Series_SeriesId",
|
||||
table: "ExternalRecommendation",
|
||||
column: "SeriesId",
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.13");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
@ -602,6 +602,27 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AppUserTableOfContent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserWantToRead", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserWantToRead");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -980,6 +1001,26 @@ namespace API.Data.Migrations
|
|||
b.ToTable("MangaFile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ManualMigrationHistory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("RanAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ManualMigrationHistory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MediaError", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1015,6 +1056,164 @@ namespace API.Data.Migrations
|
|||
b.ToTable("MediaError");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AverageScore")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("FavoriteCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ProviderUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ExternalRating");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("AniListId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("MalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("ExternalRecommendation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Body")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("BodyJustText")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RawBody")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SiteUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Tagline")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TotalVotes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ExternalReview");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AniListId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AverageExternalRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("GoogleBooksId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("MalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("ValidUntilUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeriesId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ExternalSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastChecked")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBlacklist");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1323,9 +1522,15 @@ namespace API.Data.Migrations
|
|||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ErrorDetails")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Format")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsErrored")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsProcessed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1359,8 +1564,8 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("VolumeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
b.Property<float?>("VolumeNumber")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
@ -1412,9 +1617,6 @@ namespace API.Data.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AvgHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1495,8 +1697,6 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Series");
|
||||
|
@ -1642,9 +1842,15 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("MaxHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("MaxNumber")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("MinHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("MinNumber")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -1742,6 +1948,51 @@ namespace API.Data.Migrations
|
|||
b.ToTable("CollectionTagSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("ExternalRatingsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ExternalSeriesMetadatasId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId");
|
||||
|
||||
b.HasIndex("ExternalSeriesMetadatasId");
|
||||
|
||||
b.ToTable("ExternalRatingExternalSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("ExternalRecommendationsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ExternalSeriesMetadatasId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId");
|
||||
|
||||
b.HasIndex("ExternalSeriesMetadatasId");
|
||||
|
||||
b.ToTable("ExternalRecommendationExternalSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("ExternalReviewsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ExternalSeriesMetadatasId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId");
|
||||
|
||||
b.HasIndex("ExternalSeriesMetadatasId");
|
||||
|
||||
b.ToTable("ExternalReviewExternalSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GenreSeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("GenresId")
|
||||
|
@ -2062,6 +2313,25 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserWantToRead", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("WantToRead")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Volume", "Volume")
|
||||
|
@ -2128,6 +2398,28 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithOne("ExternalSeriesMetadata")
|
||||
.HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
|
@ -2269,10 +2561,6 @@ namespace API.Data.Migrations
|
|||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", null)
|
||||
.WithMany("WantToRead")
|
||||
.HasForeignKey("AppUserId");
|
||||
|
||||
b.HasOne("API.Entities.Library", "Library")
|
||||
.WithMany("Series")
|
||||
.HasForeignKey("LibraryId")
|
||||
|
@ -2368,6 +2656,51 @@ namespace API.Data.Migrations
|
|||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Metadata.ExternalRating", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ExternalRatingsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ExternalSeriesMetadatasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Metadata.ExternalRecommendation", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ExternalRecommendationsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ExternalSeriesMetadatasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Metadata.ExternalReview", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ExternalReviewsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ExternalSeriesMetadatasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GenreSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Genre", null)
|
||||
|
@ -2510,6 +2843,8 @@ namespace API.Data.Migrations
|
|||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.Navigation("ExternalSeriesMetadata");
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Progress");
|
||||
|
|
|
@ -18,6 +18,6 @@ public class RecentlyAddedSeries
|
|||
public string? ChapterRange { get; init; }
|
||||
public string? ChapterTitle { get; init; }
|
||||
public bool IsSpecial { get; init; }
|
||||
public int VolumeNumber { get; init; }
|
||||
public float VolumeNumber { get; init; }
|
||||
public AgeRating AgeRating { get; init; }
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ public interface IAppUserProgressRepository
|
|||
Task<ProgressDto?> GetUserProgressDtoAsync(int chapterId, int userId);
|
||||
Task<bool> AnyUserProgressForSeriesAsync(int seriesId, int userId);
|
||||
Task<int> GetHighestFullyReadChapterForSeries(int seriesId, int userId);
|
||||
Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId);
|
||||
Task<float> GetHighestFullyReadVolumeForSeries(int seriesId, int userId);
|
||||
Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId);
|
||||
Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId);
|
||||
Task UpdateAllProgressThatAreMoreThanChapterPages();
|
||||
|
@ -172,14 +172,14 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d)));
|
||||
}
|
||||
|
||||
public async Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
|
||||
public async Task<float> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
|
||||
{
|
||||
var list = await _context.AppUserProgresses
|
||||
.Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id,
|
||||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
||||
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
||||
.Select(p => p.chapter.Volume.Number)
|
||||
.Select(p => p.chapter.Volume.MaxNumber)
|
||||
.ToListAsync();
|
||||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using AutoMapper.QueryableExtensions;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface IAppUserSmartFilterRepository
|
||||
{
|
||||
|
@ -55,6 +56,7 @@ public class AppUserSmartFilterRepository : IAppUserSmartFilterRepository
|
|||
|
||||
public async Task<AppUserSmartFilter?> GetById(int smartFilterId)
|
||||
{
|
||||
return await _context.AppUserSmartFilter.FirstOrDefaultAsync(d => d.Id == smartFilterId);
|
||||
return await _context.AppUserSmartFilter
|
||||
.FirstOrDefaultAsync(d => d.Id == smartFilterId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ using AutoMapper.QueryableExtensions;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
[Flags]
|
||||
public enum ChapterIncludes
|
||||
|
|
255
API/Data/Repositories/ExternalSeriesMetadataRepository.cs
Normal file
255
API/Data/Repositories/ExternalSeriesMetadataRepository.cs
Normal file
|
@ -0,0 +1,255 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services.Plus;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface IExternalSeriesMetadataRepository
|
||||
{
|
||||
void Attach(ExternalSeriesMetadata metadata);
|
||||
void Attach(ExternalRating rating);
|
||||
void Attach(ExternalReview review);
|
||||
void Remove(IEnumerable<ExternalReview>? reviews);
|
||||
void Remove(IEnumerable<ExternalRating>? ratings);
|
||||
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
|
||||
void Remove(ExternalSeriesMetadata metadata);
|
||||
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
|
||||
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
|
||||
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
|
||||
Task LinkRecommendationsToSeries(Series series);
|
||||
Task<bool> IsBlacklistedSeries(int seriesId);
|
||||
Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true);
|
||||
Task RemoveFromBlacklist(int seriesId);
|
||||
Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit);
|
||||
}
|
||||
|
||||
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ExternalSeriesMetadataRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Attach(ExternalSeriesMetadata metadata)
|
||||
{
|
||||
_context.ExternalSeriesMetadata.Attach(metadata);
|
||||
}
|
||||
|
||||
public void Attach(ExternalRating rating)
|
||||
{
|
||||
_context.ExternalRating.Attach(rating);
|
||||
}
|
||||
|
||||
public void Attach(ExternalReview review)
|
||||
{
|
||||
_context.ExternalReview.Attach(review);
|
||||
}
|
||||
|
||||
public void Remove(IEnumerable<ExternalReview>? reviews)
|
||||
{
|
||||
if (reviews == null) return;
|
||||
_context.ExternalReview.RemoveRange(reviews);
|
||||
}
|
||||
|
||||
public void Remove(IEnumerable<ExternalRating>? ratings)
|
||||
{
|
||||
if (ratings == null) return;
|
||||
_context.ExternalRating.RemoveRange(ratings);
|
||||
}
|
||||
|
||||
public void Remove(IEnumerable<ExternalRecommendation>? recommendations)
|
||||
{
|
||||
if (recommendations == null) return;
|
||||
_context.ExternalRecommendation.RemoveRange(recommendations);
|
||||
}
|
||||
|
||||
public void Remove(ExternalSeriesMetadata? metadata)
|
||||
{
|
||||
if (metadata == null) return;
|
||||
_context.ExternalSeriesMetadata.Remove(metadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ExternalSeriesMetadata entity for the given Series including all linked tables
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
public Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId)
|
||||
{
|
||||
return _context.ExternalSeriesMetadata
|
||||
.Where(s => s.SeriesId == seriesId)
|
||||
.Include(s => s.ExternalReviews)
|
||||
.Include(s => s.ExternalRatings.OrderBy(r => r.AverageScore))
|
||||
.Include(s => s.ExternalRecommendations.OrderBy(r => r.Id))
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId)
|
||||
{
|
||||
var row = await _context.ExternalSeriesMetadata
|
||||
.Where(s => s.SeriesId == seriesId)
|
||||
.FirstOrDefaultAsync();
|
||||
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId)
|
||||
{
|
||||
var seriesDetailDto = await _context.ExternalSeriesMetadata
|
||||
.Where(m => m.SeriesId == seriesId)
|
||||
.Include(m => m.ExternalRatings)
|
||||
.Include(m => m.ExternalReviews)
|
||||
.Include(m => m.ExternalRecommendations)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (seriesDetailDto == null)
|
||||
{
|
||||
return null; // or handle the case when seriesDetailDto is not found
|
||||
}
|
||||
|
||||
var externalSeriesRecommendations = seriesDetailDto.ExternalRecommendations
|
||||
.Where(r => r.SeriesId == null)
|
||||
.Select(r => _mapper.Map<ExternalSeriesDto>(r))
|
||||
.ToList();
|
||||
|
||||
var ownedIds = seriesDetailDto.ExternalRecommendations
|
||||
.Where(r => r.SeriesId != null)
|
||||
.Select(r => r.SeriesId)
|
||||
.ToList();
|
||||
|
||||
var ownedSeriesRecommendations = await _context.Series
|
||||
.Where(s => ownedIds.Contains(s.Id))
|
||||
.OrderBy(s => s.SortName.ToLower())
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
IEnumerable<UserReviewDto> reviews = new List<UserReviewDto>();
|
||||
if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any())
|
||||
{
|
||||
reviews = seriesDetailDto.ExternalReviews
|
||||
.Select(r =>
|
||||
{
|
||||
var ret = _mapper.Map<UserReviewDto>(r);
|
||||
ret.IsExternal = true;
|
||||
return ret;
|
||||
})
|
||||
.OrderByDescending(r => r.Score);
|
||||
}
|
||||
|
||||
IEnumerable<RatingDto> ratings = new List<RatingDto>();
|
||||
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Any())
|
||||
{
|
||||
ratings = seriesDetailDto.ExternalRatings
|
||||
.Select(r => _mapper.Map<RatingDto>(r));
|
||||
}
|
||||
|
||||
|
||||
var seriesDetailPlusDto = new SeriesDetailPlusDto()
|
||||
{
|
||||
Ratings = ratings,
|
||||
Reviews = reviews,
|
||||
Recommendations = new RecommendationDto()
|
||||
{
|
||||
ExternalSeries = externalSeriesRecommendations,
|
||||
OwnedSeries = ownedSeriesRecommendations
|
||||
}
|
||||
};
|
||||
|
||||
return seriesDetailPlusDto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <returns></returns>
|
||||
public async Task LinkRecommendationsToSeries(Series series)
|
||||
{
|
||||
var recMatches = await _context.ExternalRecommendation
|
||||
.Where(r => r.SeriesId == null || r.SeriesId == 0)
|
||||
.Where(r => EF.Functions.Like(r.Name, series.Name) ||
|
||||
EF.Functions.Like(r.Name, series.LocalizedName))
|
||||
.ToListAsync();
|
||||
foreach (var rec in recMatches)
|
||||
{
|
||||
rec.SeriesId = series.Id;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public Task<bool> IsBlacklistedSeries(int seriesId)
|
||||
{
|
||||
return _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance against SeriesId and Saves to the DB
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="saveChanges"></param>
|
||||
public async Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true)
|
||||
{
|
||||
if (seriesId <= 0 || await _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId)) return;
|
||||
|
||||
await _context.SeriesBlacklist.AddAsync(new SeriesBlacklist()
|
||||
{
|
||||
SeriesId = seriesId
|
||||
});
|
||||
if (saveChanges)
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the Series from Blacklist and Saves to the DB
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
public async Task RemoveFromBlacklist(int seriesId)
|
||||
{
|
||||
var seriesBlacklist = await _context.SeriesBlacklist.FirstOrDefaultAsync(sb => sb.SeriesId == seriesId);
|
||||
|
||||
if (seriesBlacklist != null)
|
||||
{
|
||||
// Remove the SeriesBlacklist entity from the context
|
||||
_context.SeriesBlacklist.Remove(seriesBlacklist);
|
||||
|
||||
// Save the changes to the database
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
||||
.Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow)
|
||||
.OrderByDescending(s => s.Library.Type)
|
||||
.ThenBy(s => s.NormalizedName)
|
||||
.Select(s => s.Id)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ public interface ILibraryRepository
|
|||
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
|
||||
IEnumerable<int> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None);
|
||||
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
||||
Task<LibraryType> GetLibraryTypeBySeriesIdAsync(int seriesId);
|
||||
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> libraryIds, LibraryIncludes includes = LibraryIncludes.None);
|
||||
Task<int> GetTotalFiles();
|
||||
IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId);
|
||||
|
@ -54,6 +55,8 @@ public interface ILibraryRepository
|
|||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
|
||||
|
||||
Task<IDictionary<int, LibraryType>> GetLibraryTypesBySeriesIdsAsync(IList<int> seriesIds);
|
||||
}
|
||||
|
||||
public class LibraryRepository : ILibraryRepository
|
||||
|
@ -106,6 +109,7 @@ public class LibraryRepository : ILibraryRepository
|
|||
return await _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
@ -141,6 +145,14 @@ public class LibraryRepository : ILibraryRepository
|
|||
.FirstAsync();
|
||||
}
|
||||
|
||||
public async Task<LibraryType> GetLibraryTypeBySeriesIdAsync(int seriesId)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
.Select(s => s.Library.Type)
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> libraryIds, LibraryIncludes includes = LibraryIncludes.None)
|
||||
{
|
||||
return await _context.Library
|
||||
|
@ -341,4 +353,16 @@ public class LibraryRepository : ILibraryRepository
|
|||
.Select(s => s.Library.AllowScrobbling)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IDictionary<int, LibraryType>> GetLibraryTypesBySeriesIdsAsync(IList<int> seriesIds)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(series => seriesIds.Contains(series.Id))
|
||||
.Select(series => new
|
||||
{
|
||||
series.Id,
|
||||
series.Library.Type
|
||||
})
|
||||
.ToDictionaryAsync(entity => entity.Id, entity => entity.Type);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public interface IScrobbleRepository
|
|||
Task ClearScrobbleErrors();
|
||||
Task<bool> HasErrorForSeries(int seriesId);
|
||||
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
|
||||
Task<IEnumerable<ScrobbleEventDto>> GetUserEvents(int userId);
|
||||
Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
|
||||
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
|
||||
}
|
||||
|
||||
|
@ -127,16 +127,17 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
return await _context.ScrobbleEvent.FirstOrDefaultAsync(e =>
|
||||
e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType);
|
||||
}
|
||||
public async Task<IEnumerable<ScrobbleEventDto>> GetUserEvents(int userId)
|
||||
|
||||
public async Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId)
|
||||
{
|
||||
return await _context.ScrobbleEvent
|
||||
.Where(e => e.AppUserId == userId)
|
||||
.Where(e => e.AppUserId == userId && !e.IsProcessed)
|
||||
.Include(e => e.Series)
|
||||
.OrderBy(e => e.LastModifiedUtc)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<ScrobbleEventDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination)
|
||||
{
|
||||
var query = _context.ScrobbleEvent
|
||||
|
@ -146,6 +147,7 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
.WhereIf(!string.IsNullOrEmpty(filter.Query), s =>
|
||||
EF.Functions.Like(s.Series.Name, $"%{filter.Query}%")
|
||||
)
|
||||
.WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<ScrobbleEventDto>(_mapper.ConfigurationProvider);
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.ManualMigrations;
|
||||
using API.Constants;
|
||||
using API.Data.Misc;
|
||||
using API.Data.Scanner;
|
||||
using API.DTOs;
|
||||
|
@ -13,8 +12,8 @@ using API.DTOs.Dashboard;
|
|||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.Search;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.DTOs.Settings;
|
||||
|
@ -27,12 +26,13 @@ using API.Extensions.QueryExtensions.Filtering;
|
|||
using API.Helpers;
|
||||
using API.Helpers.Converters;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SQLite;
|
||||
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
@ -45,7 +45,12 @@ public enum SeriesIncludes
|
|||
Metadata = 4,
|
||||
Related = 8,
|
||||
Library = 16,
|
||||
Chapters = 32
|
||||
Chapters = 32,
|
||||
ExternalReviews = 64,
|
||||
ExternalRatings = 128,
|
||||
ExternalRecommendations = 256,
|
||||
ExternalMetadata = 512
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -89,6 +94,7 @@ public interface ISeriesRepository
|
|||
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<SeriesDto?> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
||||
Task<Series?> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
|
||||
Task<IList<SeriesDto>> GetSeriesDtoByIdsAsync(IEnumerable<int> seriesIds, AppUser user);
|
||||
Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds);
|
||||
Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds);
|
||||
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
|
||||
|
@ -131,6 +137,8 @@ public interface ISeriesRepository
|
|||
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
|
||||
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
||||
Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
|
||||
public Task<IList<Series>> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId,
|
||||
MangaFormat format);
|
||||
Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
|
||||
Task<IDictionary<string, IList<SeriesModified>>> GetFolderPathMap(int libraryId);
|
||||
Task<AgeRating?> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds);
|
||||
|
@ -141,25 +149,28 @@ public interface ISeriesRepository
|
|||
Task<IDictionary<int, int>> GetLibraryIdsForSeriesAsync();
|
||||
Task<IList<SeriesMetadataDto>> GetSeriesMetadataForIds(IEnumerable<int> seriesIds);
|
||||
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
|
||||
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
|
||||
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIds(IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
|
||||
Task<int> GetAverageUserRating(int seriesId, int userId);
|
||||
Task RemoveFromOnDeck(int seriesId, int userId);
|
||||
Task ClearOnDeckRemoval(int seriesId, int userId);
|
||||
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto);
|
||||
Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
|
||||
private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled,
|
||||
Services.Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||
|
||||
public SeriesRepository(DataContext context, IMapper mapper)
|
||||
public SeriesRepository(DataContext context, IMapper mapper, UserManager<AppUser> userManager)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public void Add(Series series)
|
||||
|
@ -172,6 +183,11 @@ public class SeriesRepository : ISeriesRepository
|
|||
_context.Series.Attach(series);
|
||||
}
|
||||
|
||||
public void Attach(ExternalSeriesMetadata metadata)
|
||||
{
|
||||
_context.ExternalSeriesMetadata.Attach(metadata);
|
||||
}
|
||||
|
||||
public void Update(Series series)
|
||||
{
|
||||
_context.Entry(series).State = EntityState.Modified;
|
||||
|
@ -350,8 +366,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||
.IsRestricted(QueryContext.Search)
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(l => l.Name.ToLower())
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -370,8 +386,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(s => s.Library)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(s => s.SortName!.ToLower())
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
|
||||
.AsEnumerable();
|
||||
|
||||
|
@ -407,8 +423,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(r => r.NormalizedTitle)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -418,7 +434,6 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(c => c.Promoted || isAdmin)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(s => s.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
|
@ -430,8 +445,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SelectMany(sm => sm.People.Where(t => t.Name != null && EF.Functions.Like(t.Name, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(p => p.NormalizedName)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -440,8 +455,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -450,8 +465,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -462,14 +477,22 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SelectMany(v => v.Chapters)
|
||||
.SelectMany(c => c.Files.Select(f => f.Id));
|
||||
|
||||
result.Files = await _context.MangaFile
|
||||
.Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(f => f.FilePath)
|
||||
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
// Need to check if an admin
|
||||
var user = await _context.AppUser.FirstAsync(u => u.Id == userId);
|
||||
if (await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole))
|
||||
{
|
||||
result.Files = await _context.MangaFile
|
||||
.Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(f => f.FilePath)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Files = new List<MangaFileDto>();
|
||||
}
|
||||
|
||||
result.Chapters = await _context.Chapter
|
||||
.Include(c => c.Files)
|
||||
|
@ -478,8 +501,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
)
|
||||
.Where(c => c.Files.All(f => fileIds.Contains(f.Id)))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.TitleName)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -529,7 +552,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Volumes, Metadata, and Collection Tags
|
||||
/// Returns Full Series including all external links
|
||||
/// </summary>
|
||||
/// <param name="seriesIds"></param>
|
||||
/// <returns></returns>
|
||||
|
@ -537,14 +560,45 @@ public class SeriesRepository : ISeriesRepository
|
|||
{
|
||||
return await _context.Series
|
||||
.Include(s => s.Volumes)
|
||||
.Include(s => s.Relations)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.CollectionTags)
|
||||
.Include(s => s.Relations)
|
||||
|
||||
|
||||
.Include(s => s.ExternalSeriesMetadata)
|
||||
|
||||
.Include(s => s.ExternalSeriesMetadata)
|
||||
.ThenInclude(e => e.ExternalRatings)
|
||||
.Include(s => s.ExternalSeriesMetadata)
|
||||
.ThenInclude(e => e.ExternalReviews)
|
||||
.Include(s => s.ExternalSeriesMetadata)
|
||||
.ThenInclude(e => e.ExternalRecommendations)
|
||||
|
||||
.Where(s => seriesIds.Contains(s.Id))
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<SeriesDto>> GetSeriesDtoByIdsAsync(IEnumerable<int> seriesIds, AppUser user)
|
||||
{
|
||||
var allowedLibraries = await _context.Library
|
||||
.Where(library => library.AppUsers.Any(x => x.Id == user.Id))
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync();
|
||||
var restriction = new AgeRestriction()
|
||||
{
|
||||
AgeRating = user.AgeRestriction,
|
||||
IncludeUnknowns = user.AgeRestrictionIncludeUnknowns
|
||||
};
|
||||
return await _context.Series
|
||||
.Include(s => s.Metadata)
|
||||
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
|
||||
.RestrictAgainstAgeRestriction(restriction)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds)
|
||||
{
|
||||
var volumes = await _context.Volume
|
||||
|
@ -661,6 +715,29 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await PagedList<SeriesDto>.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
public async Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
.Select(series => new PlusSeriesDto()
|
||||
{
|
||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
SeriesName = series.Name,
|
||||
AltSeriesName = series.LocalizedName,
|
||||
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.AniListWeblinkWebsite),
|
||||
MalId = ScrobblingService.ExtractId<long?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.MalWeblinkWebsite),
|
||||
GoogleBooksId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.GoogleBooksWeblinkWebsite),
|
||||
MangaDexId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.MangaDexWeblinkWebsite),
|
||||
VolumeCount = series.Volumes.Count,
|
||||
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
|
||||
Year = series.Metadata.ReleaseYear
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task AddSeriesModifiers(int userId, IList<SeriesDto> series)
|
||||
{
|
||||
|
@ -951,6 +1028,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
SortField.TimeToRead => query.DoOrderBy(s => s.AvgHoursToRead, filter.SortOptions),
|
||||
SortField.ReleaseYear => query.DoOrderBy(s => s.Metadata.ReleaseYear, filter.SortOptions),
|
||||
SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max(), filter.SortOptions),
|
||||
SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings
|
||||
.Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), filter.SortOptions),
|
||||
_ => query
|
||||
};
|
||||
|
||||
|
@ -1003,7 +1082,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
var wantToReadStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.WantToRead);
|
||||
if (wantToReadStmt == null) return query;
|
||||
|
||||
var seriesIds = _context.AppUser.Where(u => u.Id == userId).SelectMany(u => u.WantToRead).Select(s => s.Id);
|
||||
var seriesIds = _context.AppUser.Where(u => u.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.Select(s => s.SeriesId);
|
||||
if (bool.Parse(wantToReadStmt.Value))
|
||||
{
|
||||
query = query.Where(s => seriesIds.Contains(s.Id));
|
||||
|
@ -1116,6 +1197,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value),
|
||||
FilterField.ReadTime => query.HasAverageReadTime(true, statement.Comparison, (int) value),
|
||||
FilterField.ReadingDate => query.HasReadingDate(true, statement.Comparison, (DateTime) value, userId),
|
||||
FilterField.AverageRating => query.HasAverageRating(true, statement.Comparison, (float) value),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
@ -1223,8 +1305,10 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(library => library.AppUsers.Any(x => x.Id == userId))
|
||||
.AsSplitQuery()
|
||||
.Select(l => l.Id);
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.Series
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
|
||||
.OrderBy(s => s.SortName.ToLower())
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
|
@ -1563,6 +1647,27 @@ public class SeriesRepository : ISeriesRepository
|
|||
#nullable enable
|
||||
}
|
||||
|
||||
public async Task<IList<Series>> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId,
|
||||
MangaFormat format)
|
||||
{
|
||||
var normalizedSeries = seriesName.ToNormalized();
|
||||
var normalizedLocalized = localizedName.ToNormalized();
|
||||
return await _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.Where(s => s.Format == format && format != MangaFormat.Unknown)
|
||||
.Where(s =>
|
||||
s.NormalizedName.Equals(normalizedSeries)
|
||||
|| s.NormalizedName.Equals(normalizedLocalized)
|
||||
|
||||
|| s.NormalizedLocalizedName.Equals(normalizedSeries)
|
||||
|| (!string.IsNullOrEmpty(normalizedLocalized) && s.NormalizedLocalizedName.Equals(normalizedLocalized))
|
||||
|
||||
|| (s.OriginalName != null && s.OriginalName.Equals(seriesName))
|
||||
)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes series that are not in the seenSeries list. Does not commit.
|
||||
|
@ -1789,7 +1894,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
ChapterNumber = c.Number,
|
||||
ChapterRange = c.Range,
|
||||
IsSpecial = c.IsSpecial,
|
||||
VolumeNumber = c.Volume.Number,
|
||||
VolumeNumber = c.Volume.MinNumber,
|
||||
ChapterTitle = c.Title,
|
||||
AgeRating = c.Volume.Series.Metadata.AgeRating
|
||||
})
|
||||
|
@ -1805,7 +1910,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
var query = _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => libraryIds.Contains(s.Series.LibraryId))
|
||||
.Select(w => w.Series)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
||||
|
@ -1820,7 +1926,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
var query = _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => libraryIds.Contains(s.Series.LibraryId))
|
||||
.Select(w => w.Series)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
||||
|
@ -1835,28 +1942,33 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => libraryIds.Contains(s.Series.LibraryId))
|
||||
.Select(w => w.Series)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses multiple names to find a match against a series then ensures the user has appropriate access to it. If not, returns null.
|
||||
/// Uses multiple names to find a match against a series. If not, returns null.
|
||||
/// </summary>
|
||||
/// <remarks>This does not restrict to the user at all. That is handled at the API level.</remarks>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="names"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
|
||||
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIds(IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
|
||||
{
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var libraryIds = await _context.Library.GetUserLibrariesByType(userId, libraryType).ToListAsync();
|
||||
var libraryIds = await _context.Library
|
||||
.Where(lib => lib.Type == libraryType)
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var normalizedNames = names.Select(n => n.ToNormalized()).ToList();
|
||||
SeriesDto? result = null;
|
||||
if (!string.IsNullOrEmpty(aniListUrl) || !string.IsNullOrEmpty(malUrl))
|
||||
{
|
||||
// TODO: I can likely work AniList and MalIds from ExternalSeriesMetadata in here
|
||||
result = await _context.Series
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks))
|
||||
.Where(s => libraryIds.Contains(s.Library.Id))
|
||||
.WhereIf(!string.IsNullOrEmpty(aniListUrl), s => s.Metadata.WebLinks.Contains(aniListUrl))
|
||||
|
@ -1869,7 +1981,6 @@ public class SeriesRepository : ISeriesRepository
|
|||
if (result != null) return result;
|
||||
|
||||
return await _context.Series
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Where(s => normalizedNames.Contains(s.NormalizedName) ||
|
||||
normalizedNames.Contains(s.NormalizedLocalizedName))
|
||||
.Where(s => libraryIds.Contains(s.Library.Id))
|
||||
|
@ -1886,7 +1997,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
{
|
||||
// If there is 0 or 1 rating and that rating is you, return 0 back
|
||||
var countOfRatingsThatAreUser = await _context.AppUserRating
|
||||
.Where(r => r.SeriesId == seriesId && r.HasBeenRated).CountAsync(u => u.AppUserId == userId);
|
||||
.Where(r => r.SeriesId == seriesId && r.HasBeenRated)
|
||||
.CountAsync(u => u.AppUserId == userId);
|
||||
if (countOfRatingsThatAreUser == 1)
|
||||
{
|
||||
return 0;
|
||||
|
@ -1926,7 +2038,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
return await _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead.Where(s => s.Id == seriesId && libraryIds.Contains(s.LibraryId)))
|
||||
.SelectMany(u => u.WantToRead.Where(s => s.SeriesId == seriesId && libraryIds.Contains(s.Series.LibraryId)))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.AnyAsync();
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.DTOs.Settings;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
@ -16,6 +18,7 @@ public interface ISettingsRepository
|
|||
Task<ServerSetting> GetSettingAsync(ServerSettingKey key);
|
||||
Task<IEnumerable<ServerSetting>> GetSettingsAsync();
|
||||
void Remove(ServerSetting setting);
|
||||
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
|
||||
}
|
||||
public class SettingsRepository : ISettingsRepository
|
||||
{
|
||||
|
@ -38,6 +41,13 @@ public class SettingsRepository : ISettingsRepository
|
|||
_context.Remove(setting);
|
||||
}
|
||||
|
||||
public async Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId)
|
||||
{
|
||||
return await _context.ExternalSeriesMetadata
|
||||
.Where(s => s.SeriesId == seriesId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<ServerSettingDto> GetSettingsDtoAsync()
|
||||
{
|
||||
var settings = await _context.ServerSetting
|
||||
|
|
|
@ -152,7 +152,7 @@ public class VolumeRepository : IVolumeRepository
|
|||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(vol => vol.Number)
|
||||
.OrderBy(vol => vol.MinNumber)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
@ -185,7 +185,7 @@ public class VolumeRepository : IVolumeRepository
|
|||
.ThenInclude(c => c.People)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Tags)
|
||||
.OrderBy(volume => volume.Number)
|
||||
.OrderBy(volume => volume.MinNumber)
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
|
@ -215,7 +215,7 @@ public class VolumeRepository : IVolumeRepository
|
|||
|
||||
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||
{
|
||||
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
|
||||
foreach (var v in volumes.Where(vDto => vDto.MinNumber == 0))
|
||||
{
|
||||
v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList();
|
||||
}
|
||||
|
@ -241,7 +241,9 @@ public class VolumeRepository : IVolumeRepository
|
|||
c.LastReadingProgress = progresses.Max(p => p.LastModified);
|
||||
}
|
||||
|
||||
v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);
|
||||
v.PagesRead = userProgress
|
||||
.Where(p => p.VolumeId == v.Id)
|
||||
.Sum(p => p.PagesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -208,8 +207,9 @@ public static class Seed
|
|||
{
|
||||
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
|
||||
new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
|
||||
new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"},
|
||||
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
|
||||
new() {Key = ServerSettingKey.TaskCleanup, Value = "daily"},
|
||||
new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)
|
||||
|
@ -223,12 +223,10 @@ public static class Seed
|
|||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||
new() {Key = ServerSettingKey.EnableOpds, Value = "true"},
|
||||
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
|
||||
new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
|
||||
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
||||
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
||||
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||
|
@ -241,6 +239,16 @@ public static class Seed
|
|||
new() {
|
||||
Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
|
||||
new() {Key = ServerSettingKey.EmailHost, Value = string.Empty},
|
||||
new() {Key = ServerSettingKey.EmailPort, Value = string.Empty},
|
||||
new() {Key = ServerSettingKey.EmailAuthPassword, Value = string.Empty},
|
||||
new() {Key = ServerSettingKey.EmailAuthUserName, Value = string.Empty},
|
||||
new() {Key = ServerSettingKey.EmailSenderAddress, Value = string.Empty},
|
||||
new() {Key = ServerSettingKey.EmailSenderDisplayName, Value = string.Empty},
|
||||
new() {Key = ServerSettingKey.EmailEnableSsl, Value = "true"},
|
||||
new() {Key = ServerSettingKey.EmailSizeLimit, Value = 26_214_400 + string.Empty},
|
||||
new() {Key = ServerSettingKey.EmailCustomizedTemplates, Value = "false"},
|
||||
}.ToArray());
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
|
|
|
@ -30,6 +30,7 @@ public interface IUnitOfWork
|
|||
IUserTableOfContentRepository UserTableOfContentRepository { get; }
|
||||
IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
||||
IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||
IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
|
||||
bool Commit();
|
||||
Task<bool> CommitAsync();
|
||||
bool HasChanges();
|
||||
|
@ -48,7 +49,7 @@ public class UnitOfWork : IUnitOfWork
|
|||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
|
||||
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper, _userManager);
|
||||
public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper);
|
||||
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
|
||||
|
||||
|
@ -72,6 +73,7 @@ public class UnitOfWork : IUnitOfWork
|
|||
public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper);
|
||||
public IAppUserSmartFilterRepository AppUserSmartFilterRepository => new AppUserSmartFilterRepository(_context, _mapper);
|
||||
public IAppUserExternalSourceRepository AppUserExternalSourceRepository => new AppUserExternalSourceRepository(_context, _mapper);
|
||||
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository => new ExternalSeriesMetadataRepository(_context, _mapper);
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
|
|
27
API/EmailTemplates/EmailChange.html
Normal file
27
API/EmailTemplates/EmailChange.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<tr>
|
||||
<td>
|
||||
<h1>Email Change Update</h1>
|
||||
<p>Your account's email has been updated on {{InvitingUser}}'s Kavita instance.</p>
|
||||
<p>Please click the following link to validate your email change. The email is not changed until you complete validation.</p>
|
||||
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{Link}}" target="_blank">CONFIRM EMAIL</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="small">If the button above does not work, please find the link here: <a class="small" href="{{Link}}" target="_blank">{{Link}}</a></p>
|
||||
|
||||
</td>
|
||||
</tr>
|
26
API/EmailTemplates/EmailConfirm.html
Normal file
26
API/EmailTemplates/EmailConfirm.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<tr>
|
||||
<td>
|
||||
<h1>You've Been Invited!</h1>
|
||||
<p>You have been invited to {{InvitingUser}}'s Kavita instance.</p>
|
||||
<p>Please click the following link to setup an account for yourself and start reading.</p>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{Link}}" target="_blank">ACCEPT INVITE</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="small">If the button above does not work, please find the link here: <a class="small" href="{{Link}}" target="_blank">{{Link}}</a></p>
|
||||
|
||||
</td>
|
||||
</tr>
|
27
API/EmailTemplates/EmailPasswordReset.html
Normal file
27
API/EmailTemplates/EmailPasswordReset.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<tr>
|
||||
<td>
|
||||
<h1>Forgot your password?</h1>
|
||||
<p>That's okay, it happens! Click on the button below to reset your password.</p>
|
||||
|
||||
<p>If you did not perform this action, ignore this email. Your account is safe.</p>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{Link}}" target="_blank">RESET YOUR PASSWORD</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="small">If the button above does not work, please find the link here: <a class="small" href="{{Link}}" target="_blank">{{Link}}</a></p>
|
||||
|
||||
</td>
|
||||
</tr>
|
8
API/EmailTemplates/EmailTest.html
Normal file
8
API/EmailTemplates/EmailTest.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<tr>
|
||||
<td>
|
||||
<h1>This is a Test Email</h1>
|
||||
<p>Congrats! Your instance of Kavita is setup to email correctly!</p>
|
||||
|
||||
<p>If you did not perform this action, ignore this email. Your account is safe.</p>
|
||||
</td>
|
||||
</tr>
|
6
API/EmailTemplates/SendToDevice.html
Normal file
6
API/EmailTemplates/SendToDevice.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<tr>
|
||||
<td>
|
||||
<h1>You sent file(s) from Kavita</h1>
|
||||
<p>Please find attached the file(s) you've sent.</p>
|
||||
</td>
|
||||
</tr>
|
527
API/EmailTemplates/base.html
Normal file
527
API/EmailTemplates/base.html
Normal file
|
@ -0,0 +1,527 @@
|
|||
<!doctype html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- utf-8 works for most cases -->
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<!-- Forcing initial-scale shouldn't be necessary -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!-- Use the latest (edge) version of IE rendering engine -->
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<!-- Disable auto-scale in iOS 10 Mail entirely -->
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<!-- Supports dark mode -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
|
||||
<title>Kavita - {{Preheader}}</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
|
||||
/*All the styling goes here*/
|
||||
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
|
||||
.dark-img { display: none !important; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark-img {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.light-img {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-ogsc] .dark-img {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
[data-ogsc] .light-img {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #eaebed;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
min-width: 100%;
|
||||
width: 100%; }
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #eaebed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 680px;
|
||||
width: 680px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 680px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px 40px;
|
||||
background-color: #4AC694;
|
||||
background-image: linear-gradient(#4AC694, #4AC694);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
u + .body .gmail-blend-screen {
|
||||
background:#000;
|
||||
mix-blend-mode:screen;
|
||||
}
|
||||
u + .body .gmail-blend-difference {
|
||||
background:#000;
|
||||
mix-blend-mode:difference;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark ) {
|
||||
.header {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
color: #fffffe !important;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #fffffe !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-text, .logo-text {
|
||||
font-weight: 700;
|
||||
font-family: 'Spartan', sans-serif;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
font-size: 2rem;
|
||||
vertical-align: middle;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-text img {
|
||||
vertical-align: middle;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.header-text div, .header-text p {
|
||||
vertical-align: middle;
|
||||
min-height: 75px;
|
||||
line-height: 75px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.content-block > * {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background-color: #222;
|
||||
background-image: linear-gradient(#222, #222);
|
||||
color: #fffffe;
|
||||
}
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #fffffe;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer .icon {
|
||||
margin:0;
|
||||
padding:0 10px;
|
||||
border:none;
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark ) {
|
||||
.footer {
|
||||
background-color: rgba(0,0,0,0) !important;
|
||||
background-image: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #06090f;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #4AC694;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%; }
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px; }
|
||||
.btn table {
|
||||
min-width: auto;
|
||||
width: auto;
|
||||
}
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #4AC694;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #4AC694;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #4AC694;
|
||||
background-image: linear-gradient(#4AC694, #4AC694);
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #4AC694;
|
||||
background-image: linear-gradient(#4AC694, #4AC694);
|
||||
border-color: #4AC694;
|
||||
color: #fffffe;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 10px;
|
||||
color: #222 !important;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #d5075d !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #d5075d !important;
|
||||
border-color: #d5075d !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="">
|
||||
|
||||
<!-- Visually Hidden Preheader Text-->
|
||||
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;">{{Preheader}}</div>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td class="container">
|
||||
<div class="header">
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td class="align-center" width="100%">
|
||||
<a class="header-text" href="https://www.kavitareader.com" target="_blank">
|
||||
<img class="light-img" src="https://www.kavitareader.com/img/email/email-logo.png" alt="Kavita" width="40" style="vertical-align:middle;" />
|
||||
<!--[if !mso]><! -->
|
||||
<img class="dark-img" src="https://www.kavitareader.com/img/email/email-logo.png" alt="Kavita" width="40" style="vertical-align:middle;" />
|
||||
<!--<![endif]-->
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="content">
|
||||
<table role="presentation" class="main">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{{Body}}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
<div class="gmail-blend-screen">
|
||||
<div class="gmail-blend-difference">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block" style="text-align: center;">
|
||||
<table align="center" style="text-align: center;">
|
||||
<tr>
|
||||
<td class="icon">
|
||||
<a href="https://discord.gg/b52wT37kt7" target="_blank">
|
||||
Discord
|
||||
</a>
|
||||
</td>
|
||||
<td class="icon">
|
||||
<a href="https://github.com/Kareadita/Kavita/">
|
||||
Github
|
||||
</a>
|
||||
</td>
|
||||
<td class="icon">
|
||||
<a href="https://wiki.kavitareader.com/en">
|
||||
Wiki
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -31,7 +31,7 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
|||
/// <summary>
|
||||
/// A list of Series the user want's to read
|
||||
/// </summary>
|
||||
public ICollection<Series> WantToRead { get; set; } = null!;
|
||||
public ICollection<AppUserWantToRead> WantToRead { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// A list of Devices which allows the user to send files to
|
||||
/// </summary>
|
||||
|
|
20
API/Entities/AppUserWantToRead.cs
Normal file
20
API/Entities/AppUserWantToRead.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
namespace API.Entities;
|
||||
|
||||
public class AppUserWantToRead
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public required int SeriesId { get; set; }
|
||||
public virtual Series Series { get; set; }
|
||||
|
||||
|
||||
// Relationships
|
||||
/// <summary>
|
||||
/// Navigational Property for EF. Links to a unique AppUser
|
||||
/// </summary>
|
||||
public AppUser AppUser { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// User this table of content belongs to
|
||||
/// </summary>
|
||||
public int AppUserId { get; set; }
|
||||
}
|
|
@ -25,8 +25,13 @@ public enum LibraryType
|
|||
[Description("Image")]
|
||||
Image = 3,
|
||||
/// <summary>
|
||||
/// Allows Books to Scrobble with AniList for Kavita+
|
||||
/// </summary>
|
||||
[Description("Light Novel")]
|
||||
LightNovel = 4,
|
||||
/// <summary>
|
||||
/// Uses Magazine regex and is restricted to PDF and Archive by default
|
||||
/// </summary>
|
||||
[Description("Magazine")]
|
||||
Magazine = 4
|
||||
Magazine = 5
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ public enum ServerSettingKey
|
|||
/// Is Authentication needed for non-admin accounts
|
||||
/// </summary>
|
||||
/// <remarks>Deprecated. This is no longer used v0.5.1+. Assume Authentication is always in effect</remarks>
|
||||
[Obsolete("Not supported as of v0.5.1")]
|
||||
[Description("EnableAuthentication")]
|
||||
EnableAuthentication = 8,
|
||||
/// <summary>
|
||||
|
@ -79,6 +80,7 @@ public enum ServerSettingKey
|
|||
/// If SMTP is enabled on the server
|
||||
/// </summary>
|
||||
[Description("CustomEmailService")]
|
||||
[Obsolete("Use Email settings instead")]
|
||||
EmailServiceUrl = 13,
|
||||
/// <summary>
|
||||
/// If Kavita should save bookmarks as WebP images
|
||||
|
@ -147,6 +149,42 @@ public enum ServerSettingKey
|
|||
/// The size of the cover image thumbnail. Defaults to <see cref="CoverImageSize"/>.Default
|
||||
/// </summary>
|
||||
[Description("CoverImageSize")]
|
||||
CoverImageSize = 27
|
||||
|
||||
CoverImageSize = 27,
|
||||
#region EmailSettings
|
||||
/// <summary>
|
||||
/// The address of the emailer host
|
||||
/// </summary>
|
||||
[Description("EmailSenderAddress")]
|
||||
EmailSenderAddress = 28,
|
||||
/// <summary>
|
||||
/// What the email name should be
|
||||
/// </summary>
|
||||
[Description("EmailSenderDisplayName")]
|
||||
EmailSenderDisplayName = 29,
|
||||
[Description("EmailAuthUserName")]
|
||||
EmailAuthUserName = 30,
|
||||
[Description("EmailAuthPassword")]
|
||||
EmailAuthPassword = 31,
|
||||
[Description("EmailHost")]
|
||||
EmailHost = 32,
|
||||
[Description("EmailPort")]
|
||||
EmailPort = 33,
|
||||
[Description("EmailEnableSsl")]
|
||||
EmailEnableSsl = 34,
|
||||
/// <summary>
|
||||
/// Number of bytes that the sender allows to be sent through
|
||||
/// </summary>
|
||||
[Description("EmailSizeLimit")]
|
||||
EmailSizeLimit = 35,
|
||||
/// <summary>
|
||||
/// Should Kavita use config/templates for Email templates or the default ones
|
||||
/// </summary>
|
||||
[Description("EmailCustomizedTemplates")]
|
||||
EmailCustomizedTemplates = 36,
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// When the cleanup task should run - Critical to keeping Kavita working
|
||||
/// </summary>
|
||||
[Description("TaskCleanup")]
|
||||
TaskCleanup = 37
|
||||
}
|
||||
|
|
14
API/Entities/ManualMigrationHistory.cs
Normal file
14
API/Entities/ManualMigrationHistory.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
|
||||
namespace API.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed
|
||||
/// </summary>
|
||||
public class ManualMigrationHistory
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string ProductVersion { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public DateTime RanAt { get; set; }
|
||||
}
|
17
API/Entities/Metadata/ExternalRating.cs
Normal file
17
API/Entities/Metadata/ExternalRating.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
using API.Services.Plus;
|
||||
|
||||
namespace API.Entities.Metadata;
|
||||
|
||||
public class ExternalRating
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int AverageScore { get; set; }
|
||||
public int FavoriteCount { get; set; }
|
||||
public ScrobbleProvider Provider { get; set; }
|
||||
public string? ProviderUrl { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
|
||||
public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!;
|
||||
}
|
29
API/Entities/Metadata/ExternalRecommendation.cs
Normal file
29
API/Entities/Metadata/ExternalRecommendation.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Collections.Generic;
|
||||
using API.Services.Plus;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
||||
namespace API.Entities.Metadata;
|
||||
|
||||
[Index(nameof(SeriesId), IsUnique = false)]
|
||||
public class ExternalRecommendation
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public required string Name { get; set; }
|
||||
public required string CoverUrl { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public int? AniListId { get; set; }
|
||||
public long? MalId { get; set; }
|
||||
public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList;
|
||||
|
||||
/// <summary>
|
||||
/// When null, represents an external series. When set, it is a Series
|
||||
/// </summary>
|
||||
public int? SeriesId { get; set; }
|
||||
//public virtual Series? Series { get; set; }
|
||||
|
||||
// Relationships
|
||||
public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!;
|
||||
}
|
43
API/Entities/Metadata/ExternalReview.cs
Normal file
43
API/Entities/Metadata/ExternalReview.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using API.Services.Plus;
|
||||
|
||||
namespace API.Entities.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Externally supplied Review for a given Series
|
||||
/// </summary>
|
||||
public class ExternalReview
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
public required string Body { get; set; }
|
||||
/// <summary>
|
||||
/// Pure text version of the body
|
||||
/// </summary>
|
||||
public required string BodyJustText { get; set; }
|
||||
/// <summary>
|
||||
/// Raw from the provider. Usually Markdown
|
||||
/// </summary>
|
||||
public string RawBody { get; set; }
|
||||
public required ScrobbleProvider Provider { get; set; }
|
||||
public string SiteUrl { get; set; }
|
||||
/// <summary>
|
||||
/// Reviewer's username
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
/// <summary>
|
||||
/// An Optional Rating coming from the Review
|
||||
/// </summary>
|
||||
public int Rating { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// The media's overall Score
|
||||
/// </summary>
|
||||
public int Score { get; set; }
|
||||
public int TotalVotes { get; set; }
|
||||
|
||||
|
||||
public int SeriesId { get; set; }
|
||||
|
||||
// Relationships
|
||||
public ICollection<ExternalSeriesMetadata> ExternalSeriesMetadatas { get; set; } = null!;
|
||||
}
|
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