Security Event Logging & Bugfixes (#1882)

* Fixed bookmarking failing to convert to webp

* Brought the ag-swipe/ng-swipe code into Kavita due to being abandoned by developer and angular requirements.

* Fixed average reading time per week finally

* Cleaned up some extra decimals on time duration pipe

* Don't try to update index.html for base url on local. Fixed ag-swipe on prod mode.

* Updated a link on theme manager to point to the new github

* Range knobs should be primary color on firefox too

* Implemented the ability to get thumbnails of pages inside an archive or pdf.

* Updated packages and fixed opds-ps 1.2 issue

* Fixed lock file

* Allow Kavita's Swagger to hit instances with CORS

* Added IP/Request logging for Security Audits

* Linked up Summary tag from CBL into Kavita.

* Redid the migration so SecurityEvent now has UTC date as well.

* Split security logging to a separate file

* Update to new versions of checkout and setup

* Added a PR check on PR body to ensure that it doesn't contain any characters that break our discord hook.

* Updating action

* optimize regex in action

* Fixed an issue where fit to width would cause the actual height of the image to be shown for pagination bars, instead of rendered.

* Added some new code in GetPageFromFiles to ensure pages that exceed array map down to last file.

* Added comment about robots

* Fixed up unit tests for new ReaderService signature

* Kavita now cleans up empty reading lists at night

* Don't allow nightly cleanup to run if we are running media conversion tasks

* Fixed some bugs in typeahead, it should behave much more reliably.

* Fix an issue where emulate comic book wasn't extending to the bottom properly

* Added support for Series Chapter 001 Volume 001

* Refactor XFrameOptions="SameOrigins" out to allow users to override in appsettings.json.

* Added a rate limiter for some endpoints, but it doesn't seem to be triggering

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2023-03-16 15:57:34 -05:00 committed by GitHub
parent 21203414f0
commit c10acb1279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 2890 additions and 302 deletions

View File

@ -8,17 +8,25 @@ on:
types: [synchronize]
jobs:
check_pr:
runs-on: ubuntu-latest
steps:
- name: Check PR Body
uses: JJ/github-pr-contains-action@releases/v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
bodyDoesNotContain: "["|`]"
build:
name: Build .Net
runs-on: windows-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET Core
uses: actions/setup-dotnet@v2
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
@ -74,19 +82,18 @@ jobs:
- name: Test
run: dotnet test --no-restore --verbosity normal
version:
name: Bump version on Develop push
needs: [ build ]
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET Core
uses: actions/setup-dotnet@v2
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
@ -139,7 +146,7 @@ jobs:
echo "::set-output name=BODY::$body"
- name: Check Out Repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: develop
@ -176,7 +183,7 @@ jobs:
run: echo "${{steps.get-version.outputs.assembly-version}}"
- name: Compile dotnet app
uses: actions/setup-dotnet@v2
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
@ -253,7 +260,7 @@ jobs:
echo "::set-output name=BODY::$body"
- name: Check Out Repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: main
@ -293,7 +300,7 @@ jobs:
id: parse-version
- name: Compile dotnet app
uses: actions/setup-dotnet@v2
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Install Swashbuckle CLI

View File

@ -6,11 +6,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NSubstitute" Version="4.4.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.1" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.4" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -9,6 +9,7 @@ namespace API.Tests.Helpers;
public class PersonHelperTests
{
#region UpdatePeople
[Fact]
public void UpdatePeople_ShouldAddNewPeople()
{
@ -47,7 +48,15 @@ public class PersonHelperTests
Assert.Equal(3, allPeople.Count);
}
#endregion
#region UpdatePeopleList
#endregion
#region RemovePeople
[Fact]
public void RemovePeople_ShouldRemovePeopleOfSameRole()
{
@ -111,6 +120,10 @@ public class PersonHelperTests
Assert.Equal(2, peopleRemoved.Count);
}
#endregion
#region KeepOnlySamePeopleBetweenLists
[Fact]
public void KeepOnlySamePeopleBetweenLists()
{
@ -135,6 +148,9 @@ public class PersonHelperTests
Assert.Equal(2, peopleRemoved.Count);
}
#endregion
#region AddPeople
[Fact]
public void AddPeople_ShouldAddOnlyNonExistingPeople()
@ -157,4 +173,6 @@ public class PersonHelperTests
Assert.Equal(4, existingPeople.Count);
}
#endregion
}

View File

@ -81,6 +81,7 @@ public class MangaParserTests
[InlineData("몰?루 아카이브 7.5권", "7.5")]
[InlineData("63권#200", "63")]
[InlineData("시즌34삽화2", "34")]
[InlineData("Accel World Chapter 001 Volume 002", "2")]
public void ParseVolumeTest(string filename, string expected)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
@ -195,6 +196,7 @@ public class MangaParserTests
[InlineData("Манга Том 1 3-4 Глава", "Манга")]
[InlineData("Esquire 6권 2021년 10월호", "Esquire")]
[InlineData("Accel World: Vol 1", "Accel World")]
[InlineData("Accel World Chapter 001 Volume 002", "Accel World")]
public void ParseSeriesTest(string filename, string expected)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
@ -278,6 +280,7 @@ public class MangaParserTests
[InlineData("Манга Глава 2", "2")]
[InlineData("Манга 2 Глава", "2")]
[InlineData("Манга Том 1 2 Глава", "2")]
[InlineData("Accel World Chapter 001 Volume 002", "1")]
public void ParseChaptersTest(string filename, string expected)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Threading.Tasks;
@ -16,7 +17,6 @@ using API.Helpers.Builders;
using API.Services;
using API.Services.Tasks;
using API.SignalR;
using API.Tests.Helpers;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
@ -437,7 +437,8 @@ public class CleanupServiceTests : AbstractDbTest
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>(),
Substitute.For<IImageService>(), new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkChaptersUntilAsRead(user, 1, 5);
@ -534,7 +535,8 @@ public class CleanupServiceTests : AbstractDbTest
await _unitOfWork.CommitAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
Substitute.For<IEventHub>());
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
await readerService.MarkSeriesAsRead(user, s.Id);
await _unitOfWork.CommitAsync();

View File

@ -29,10 +29,9 @@ namespace API.Tests.Services;
public class ReaderServiceTests
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly IUnitOfWork _unitOfWork;
private readonly DataContext _context;
private readonly ReaderService _readerService;
private const string CacheDirectory = "C:/kavita/config/cache/";
private const string CoverImageDirectory = "C:/kavita/config/covers/";
@ -50,6 +49,9 @@ public class ReaderServiceTests
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
var mapper = config.CreateMapper();
_unitOfWork = new UnitOfWork(_context, mapper, null);
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
}
#region Setup
@ -147,10 +149,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
Assert.Equal(0, await readerService.CapPageToChapter(1, -1));
Assert.Equal(1, await readerService.CapPageToChapter(1, 10));
Assert.Equal(0, await _readerService.CapPageToChapter(1, -1));
Assert.Equal(1, await _readerService.CapPageToChapter(1, 10));
}
#endregion
@ -186,9 +187,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var successful = await readerService.SaveReadingProgress(new ProgressDto()
var successful = await _readerService.SaveReadingProgress(new ProgressDto()
{
ChapterId = 1,
PageNum = 1,
@ -230,9 +231,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var successful = await readerService.SaveReadingProgress(new ProgressDto()
var successful = await _readerService.SaveReadingProgress(new ProgressDto()
{
ChapterId = 1,
PageNum = 1,
@ -244,7 +245,7 @@ public class ReaderServiceTests
Assert.True(successful);
Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1));
Assert.True(await readerService.SaveReadingProgress(new ProgressDto()
Assert.True(await _readerService.SaveReadingProgress(new ProgressDto()
{
ChapterId = 1,
PageNum = 1,
@ -294,10 +295,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1);
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
await _context.SaveChangesAsync();
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
@ -338,15 +339,15 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
await _context.SaveChangesAsync();
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
await readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
await _readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
await _context.SaveChangesAsync();
var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses;
@ -427,9 +428,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 1, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("2", actualChapter.Range);
}
@ -475,10 +476,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("21", actualChapter.Range);
}
@ -524,10 +525,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("21", actualChapter.Range);
}
@ -566,10 +567,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1);
Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("1", actualChapter.Range);
@ -614,9 +615,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("0", actualChapter.Range);
@ -658,10 +659,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1);
Assert.Equal(-1, nextChapter);
}
@ -693,10 +694,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
Assert.Equal(-1, nextChapter);
}
@ -735,10 +736,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
Assert.Equal(-1, nextChapter);
}
@ -777,10 +778,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("A.cbz", actualChapter.Range);
@ -815,10 +816,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("A.cbz", actualChapter.Range);
@ -857,10 +858,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 3, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 3, 1);
Assert.Equal(-1, nextChapter);
}
@ -898,10 +899,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1);
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("B.cbz", actualChapter.Range);
@ -952,9 +953,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.Equal("1", actualChapter.Range);
}
@ -998,9 +999,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 3, 5, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 3, 5, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.Equal("22", actualChapter.Range);
}
@ -1044,10 +1045,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// prevChapter should be id from ch.21 from volume 2001
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 4, 7, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 4, 7, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.NotNull(actualChapter);
@ -1091,10 +1092,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.Equal("2", actualChapter.Range);
}
@ -1133,10 +1134,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
Assert.Equal(2, prevChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.Equal("2", actualChapter.Range);
@ -1170,10 +1171,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
Assert.Equal(-1, prevChapter);
}
@ -1204,10 +1205,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
Assert.Equal(-1, prevChapter);
}
@ -1244,10 +1245,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
Assert.Equal(-1, prevChapter);
}
@ -1293,14 +1294,14 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2,5, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,5, 1);
var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter);
Assert.Equal(1, float.Parse(chapterInfoDto.ChapterNumber));
// This is first chapter of first volume
prevChapter = await readerService.GetPrevChapterIdAsync(1, 2,4, 1);
prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,4, 1);
Assert.Equal(-1, prevChapter);
}
@ -1332,10 +1333,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
Assert.Equal(-1, prevChapter);
}
@ -1374,10 +1375,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 4, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 4, 1);
Assert.NotEqual(-1, prevChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.Equal("A.cbz", actualChapter.Range);
@ -1418,10 +1419,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
Assert.NotEqual(-1, prevChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.Equal("22", actualChapter.Range);
@ -1472,9 +1473,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("1", nextChapter.Range);
}
@ -1511,15 +1512,15 @@ public class ReaderServiceTests
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 2,
ChapterId = 1,
SeriesId = 1,
VolumeId = 1
}, 1);
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("1", nextChapter.Range);
}
@ -1560,24 +1561,24 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// Save progress on first volume chapters and 1st of second volume
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 1,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 2,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 3,
@ -1587,7 +1588,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("22", nextChapter.Range);
@ -1638,10 +1639,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// Save progress on first volume and 1st chapter of second volume
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 6, // Chapter 0 volume 1 id
@ -1650,7 +1651,7 @@ public class ReaderServiceTests
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 7, // Chapter 21 volume 2 id
@ -1660,7 +1661,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("22", nextChapter.Range);
@ -1702,24 +1703,24 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// Save progress on first volume chapters and 1st of second volume
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 1,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 2,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 3,
@ -1729,7 +1730,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("31", nextChapter.Range);
}
@ -1769,8 +1770,8 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("1", nextChapter.Range);
}
@ -1811,21 +1812,21 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// Mark everything but chapter 101 as read
await readerService.MarkSeriesAsRead(user, 1);
await _readerService.MarkSeriesAsRead(user, 1);
await _unitOfWork.CommitAsync();
// Unmark last chapter as read
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 0,
ChapterId = (await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1)).Chapters.ElementAt(1).Id,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 0,
ChapterId = (await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1)).Chapters.ElementAt(2).Id,
@ -1834,7 +1835,7 @@ public class ReaderServiceTests
}, 1);
await _context.SaveChangesAsync();
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("101", nextChapter.Range);
}
@ -1869,24 +1870,24 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// Save progress on first volume chapters and 1st of second volume
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 1,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 2,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 3,
@ -1896,7 +1897,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("1", nextChapter.Range);
}
@ -1933,14 +1934,14 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// Save progress on first volume chapters and 1st of second volume
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
await readerService.MarkSeriesAsRead(user, 1);
await _readerService.MarkSeriesAsRead(user, 1);
await _context.SaveChangesAsync();
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("11", nextChapter.Range);
}
@ -1973,24 +1974,24 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// Save progress on first volume chapters and 1st of second volume
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 1,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 2,
SeriesId = 1,
VolumeId = 1
}, 1);
await readerService.SaveReadingProgress(new ProgressDto()
await _readerService.SaveReadingProgress(new ProgressDto()
{
PageNum = 1,
ChapterId = 3,
@ -2000,7 +2001,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("Some Special Title", nextChapter.Range);
}
@ -2041,9 +2042,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
await readerService.MarkSeriesAsRead(user, 1);
await _readerService.MarkSeriesAsRead(user, 1);
await _context.SaveChangesAsync();
// Add 2 new unread series to the Series
@ -2053,7 +2054,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
// This tests that if you add a series later to a volume and a loose leaf chapter, we continue from that volume, rather than loose leaf
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal("14.9", nextChapter.Range);
}
@ -2104,18 +2105,18 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
// Save progress on first volume chapters and 1st of second volume
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
await readerService.MarkChaptersAsRead(user, 1,
await _readerService.MarkChaptersAsRead(user, 1,
new List<Chapter>()
{
readChapter1, readChapter2
});
await _context.SaveChangesAsync();
var nextChapter = await readerService.GetContinuePoint(1, 1);
var nextChapter = await _readerService.GetContinuePoint(1, 1);
Assert.Equal(4, nextChapter.VolumeId);
}
@ -2152,10 +2153,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkChaptersUntilAsRead(user, 1, 5);
await _readerService.MarkChaptersUntilAsRead(user, 1, 5);
await _context.SaveChangesAsync();
// Validate correct chapters have read status
@ -2194,10 +2195,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkChaptersUntilAsRead(user, 1, 2.5f);
await _readerService.MarkChaptersUntilAsRead(user, 1, 2.5f);
await _context.SaveChangesAsync();
// Validate correct chapters have read status
@ -2236,10 +2237,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkChaptersUntilAsRead(user, 1, 2);
await _readerService.MarkChaptersUntilAsRead(user, 1, 2);
await _context.SaveChangesAsync();
// Validate correct chapters have read status
@ -2289,12 +2290,12 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
const int markReadUntilNumber = 47;
await readerService.MarkChaptersUntilAsRead(user, 1, markReadUntilNumber);
await _readerService.MarkChaptersUntilAsRead(user, 1, markReadUntilNumber);
await _context.SaveChangesAsync();
var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(1, 1);
@ -2347,9 +2348,9 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
await readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
await _readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
await _context.SaveChangesAsync();
Assert.Equal(4, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
@ -2386,15 +2387,15 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
await readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
await _context.SaveChangesAsync();
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
await readerService.MarkSeriesAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
await _readerService.MarkSeriesAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
await _context.SaveChangesAsync();
var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses;
@ -2476,10 +2477,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkVolumesUntilAsRead(user, 1, 2002);
await _readerService.MarkVolumesUntilAsRead(user, 1, 2002);
await _context.SaveChangesAsync();
// Validate loose leaf chapters don't get marked as read
@ -2534,10 +2535,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkVolumesUntilAsRead(user, 1, 2002);
await _readerService.MarkVolumesUntilAsRead(user, 1, 2002);
await _context.SaveChangesAsync();
// Validate loose leaf chapters don't get marked as read
@ -2577,9 +2578,9 @@ public class ReaderServiceTests
new [] {"0,0", "1,1", "2,1", "3,3", "4,3", "5,5", "6,6", "7,6", "8,8", "9,9"})]
public void GetPairs_ShouldReturnPairsForNoWideImages(string caseName, IList<bool> wides, IList<string> expectedPairs)
{
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var files = wides.Select((b, i) => new FileDimensionDto() {PageNumber = i, Height = 1, Width = 1, FileName = string.Empty, IsWide = b}).ToList();
var pairs = readerService.GetPairs(files);
var pairs = _readerService.GetPairs(files);
var expectedDict = new Dictionary<int, int>();
foreach (var pair in expectedPairs)
{

View File

@ -500,7 +500,8 @@ public class ReadingListServiceTests
Assert.Equal(3, readingList.Items.Count);
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
Substitute.For<IEventHub>());
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
// Mark 2 as fully read
await readerService.MarkChaptersAsRead(user, 1,
(await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(new List<int>() {2})).ToList());

View File

@ -27,6 +27,8 @@ public class TachiyomiServiceTests
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly DataContext _context;
private readonly ReaderService _readerService;
private readonly TachiyomiService _tachiyomiService;
private const string CacheDirectory = "C:/kavita/config/cache/";
private const string CoverImageDirectory = "C:/kavita/config/covers/";
private const string BackupDirectory = "C:/kavita/config/backups/";
@ -44,6 +46,10 @@ public class TachiyomiServiceTests
_mapper = config.CreateMapper();
_unitOfWork = new UnitOfWork(_context, _mapper, null);
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
_tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), _readerService);
}
@ -151,10 +157,7 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Null(latestChapter);
}
@ -201,16 +204,14 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkSeriesAsRead(user,1);
await _readerService.MarkSeriesAsRead(user,1);
await _context.SaveChangesAsync();
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Equal("96", latestChapter.Number);
}
@ -257,16 +258,14 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
await _context.SaveChangesAsync();
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Equal("21", latestChapter.Number);
}
@ -312,17 +311,15 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
await _context.SaveChangesAsync();
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Equal("0.0001", latestChapter.Number);
}
@ -362,17 +359,15 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkSeriesAsRead(user, 1);
await _readerService.MarkSeriesAsRead(user, 1);
await _context.SaveChangesAsync();
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Equal("0.0003", latestChapter.Number);
}
@ -417,17 +412,14 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await tachiyomiService.MarkChaptersUntilAsRead(user,1,2002/10_000F);
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,2002/10_000F);
await _context.SaveChangesAsync();
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Equal("0.2002", latestChapter.Number);
}
@ -478,10 +470,7 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Null(latestChapter);
}
@ -527,16 +516,13 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkSeriesAsRead(user,1);
await _readerService.MarkSeriesAsRead(user,1);
await _context.SaveChangesAsync();
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Equal("96", latestChapter.Number);
}
@ -583,16 +569,13 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
await _context.SaveChangesAsync();
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Equal("21", latestChapter.Number);
}
@ -637,17 +620,14 @@ public class TachiyomiServiceTests
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
await _context.SaveChangesAsync();
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
Assert.Equal("0.0001", latestChapter.Number);
}

View File

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
@ -26,7 +28,8 @@ public class WordCountAnalysisTests : AbstractDbTest
public WordCountAnalysisTests() : base()
{
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
Substitute.For<IEventHub>());
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
}
protected override async Task ResetDb()

View File

@ -67,15 +67,15 @@
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.3" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageReference Include="NetVips" Version="2.2.0" />
@ -92,14 +92,14 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.53.0.62665">
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.54.0.64047">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.27.0" />
<PackageReference Include="System.IO.Abstractions" Version="19.2.1" />
<PackageReference Include="System.IO.Abstractions" Version="19.2.4" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
</ItemGroup>

View File

@ -13,6 +13,7 @@ using API.Entities;
using API.Entities.Enums;
using API.Errors;
using API.Extensions;
using API.Middleware.RateLimit;
using API.Services;
using API.SignalR;
using AutoMapper;
@ -22,6 +23,7 @@ using Kavita.Common.EnvironmentInfo;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -769,6 +771,7 @@ public class AccountController : BaseApiController
/// <returns></returns>
[AllowAnonymous]
[HttpPost("forgot-password")]
[EnableRateLimiting("Authentication")]
public async Task<ActionResult<string>> ForgotPassword([FromQuery] string email)
{
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
@ -847,6 +850,7 @@ public class AccountController : BaseApiController
/// <param name="userId"></param>
/// <returns></returns>
[HttpPost("resend-confirmation-email")]
[EnableRateLimiting("Authentication")]
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);

View File

@ -904,8 +904,11 @@ public class OpdsController : BaseApiController
var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg",
$"{Prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}");
link.TotalPages = mangaFile.Pages;
link.LastRead = progress.PageNum;
link.LastReadDate = progress.LastModifiedUtc;
if (progress != null)
{
link.LastRead = progress.PageNum;
link.LastReadDate = progress.LastModifiedUtc;
}
link.IsPageStream = true;
return link;
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -34,12 +34,14 @@ public class ReaderController : BaseApiController
private readonly IBookmarkService _bookmarkService;
private readonly IAccountService _accountService;
private readonly IEventHub _eventHub;
private readonly IImageService _imageService;
private readonly IDirectoryService _directoryService;
/// <inheritdoc />
public ReaderController(ICacheService cacheService,
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
IReaderService readerService, IBookmarkService bookmarkService,
IAccountService accountService, IEventHub eventHub)
IAccountService accountService, IEventHub eventHub, IImageService imageService, IDirectoryService directoryService)
{
_cacheService = cacheService;
_unitOfWork = unitOfWork;
@ -48,6 +50,8 @@ public class ReaderController : BaseApiController
_bookmarkService = bookmarkService;
_accountService = accountService;
_eventHub = eventHub;
_imageService = imageService;
_directoryService = directoryService;
}
/// <summary>
@ -114,6 +118,20 @@ public class ReaderController : BaseApiController
}
}
[HttpGet("thumbnail")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
[AllowAnonymous]
public async Task<ActionResult> GetThumbnail(int chapterId, int pageNum)
{
var chapter = await _cacheService.Ensure(chapterId, true);
if (chapter == null) return BadRequest("There was an issue extracting images from chapter");
var images = _cacheService.GetCachedPages(chapterId);
var path = await _readerService.GetThumbnail(chapter, pageNum, images);
var format = Path.GetExtension(path).Replace(".", string.Empty); // TODO: Make this an extension
return PhysicalFile(path, "image/" + format, Path.GetFileName(path), true);
}
/// <summary>
/// Returns an image for a given bookmark series. Side effect: This will cache the bookmark images for reading.
/// </summary>
@ -172,13 +190,14 @@ public class ReaderController : BaseApiController
/// <summary>
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
/// </summary>
/// <remarks>This is generally the first call when attempting to read to allow pre-generation of assets needed for reading</remarks>
/// <param name="chapterId"></param>
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
/// <param name="includeDimensions">Include file dimensions. Only useful for image based reading</param>
/// <returns></returns>
[HttpGet("chapter-info")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf", "includeDimensions"})]
public async Task<ActionResult<ChapterInfoDto?>> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false)
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false)
{
if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore
var chapter = await _cacheService.Ensure(chapterId, extractPdf);

View File

@ -47,6 +47,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<FolderPath> FolderPath { get; set; } = null!;
public DbSet<Device> Device { get; set; } = null!;
public DbSet<ServerStatistics> ServerStatistics { get; set; } = null!;
public DbSet<SecurityEvent> SecurityEvent { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class SecurityEvent : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SecurityEvent",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
IpAddress = table.Column<string>(type: "TEXT", nullable: true),
RequestMethod = table.Column<string>(type: "TEXT", nullable: true),
RequestPath = table.Column<string>(type: "TEXT", nullable: true),
UserAgent = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SecurityEvent", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SecurityEvent");
}
}
}

View File

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
@ -944,6 +944,35 @@ namespace API.Data.Migrations
b.ToTable("ReadingListItem");
});
modelBuilder.Entity("API.Entities.SecurityEvent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("IpAddress")
.HasColumnType("TEXT");
b.Property<string>("RequestMethod")
.HasColumnType("TEXT");
b.Property<string>("RequestPath")
.HasColumnType("TEXT");
b.Property<string>("UserAgent")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("SecurityEvent");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.Property<int>("Id")

View File

@ -47,6 +47,7 @@ public interface IReadingListRepository
IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId);
Task<IList<ReadingList>> GetAllWithNonWebPCovers();
Task<IList<string>> GetFirstFourCoverImagesByReadingListId(int readingListId);
Task<int> RemoveReadingListsWithoutSeries();
}
public class ReadingListRepository : IReadingListRepository
@ -132,6 +133,18 @@ public class ReadingListRepository : IReadingListRepository
.ToListAsync();
}
public async Task<int> RemoveReadingListsWithoutSeries()
{
var listsToDelete = await _context.ReadingList
.Include(c => c.Items)
.Where(c => c.Items.Count == 0)
.AsSplitQuery()
.ToListAsync();
_context.RemoveRange(listsToDelete);
return await _context.SaveChangesAsync();
}
public void Remove(ReadingListItem item)
{
_context.ReadingListItem.Remove(item);

View File

@ -0,0 +1,27 @@
using API.Entities;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
public interface ISecurityEventRepository
{
void Add(SecurityEvent securityEvent);
}
public class SecurityEventRepository : ISecurityEventRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public SecurityEventRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Add(SecurityEvent securityEvent)
{
_context.SecurityEvent.Add(securityEvent);
}
}

View File

@ -25,6 +25,7 @@ public interface IUnitOfWork
ISiteThemeRepository SiteThemeRepository { get; }
IMangaFileRepository MangaFileRepository { get; }
IDeviceRepository DeviceRepository { get; }
ISecurityEventRepository SecurityEventRepository { get; }
bool Commit();
Task<bool> CommitAsync();
bool HasChanges();
@ -62,6 +63,7 @@ public class UnitOfWork : IUnitOfWork
public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper);
public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context);
public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper);
public ISecurityEventRepository SecurityEventRepository => new SecurityEventRepository(_context, _mapper);
/// <summary>
/// Commits changes to the DB. Completes the open transaction.

View File

@ -0,0 +1,14 @@
using System;
namespace API.Entities;
public class SecurityEvent
{
public int Id { get; set; }
public string IpAddress { get; set; }
public string RequestMethod { get; set; }
public string RequestPath { get; set; }
public string UserAgent { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime CreatedAtUtc { get; set; }
}

View File

@ -7,6 +7,7 @@ using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Builders;
namespace API.Helpers;
@ -156,7 +157,7 @@ public static class PersonHelper
else
{
// Add new tag
handleAdd(DbFactory.Person(tag.Name, role));
handleAdd(new PersonBuilder(tag.Name, role).Build());
isModified = true;
}
}

View File

@ -1,6 +1,7 @@
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Filters;
using Serilog.Formatting.Display;
namespace API.Logging;
@ -12,6 +13,7 @@ namespace API.Logging;
public static class LogLevelOptions
{
public const string LogFile = "config/logs/kavita.log";
public const string SecurityLogFile = "config/logs/security.log";
public const bool LogRollingEnabled = true;
/// <summary>
/// Controls the Logging Level of the Application

View File

@ -0,0 +1,36 @@
using System;
using System.Globalization;
using System.Threading;
using System.Threading.RateLimiting;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
namespace API.Middleware.RateLimit;
public class AuthenticationRateLimiterPolicy : IRateLimiterPolicy<string>
{
public RateLimitPartition<string> GetPartition(HttpContext httpContext)
{
return RateLimitPartition.GetFixedWindowLimiter(httpContext.Request.Headers.Host.ToString(),
partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 1,
Window = TimeSpan.FromMinutes(10),
});
}
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } =
(context, _) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
return new ValueTask();
};
}

View File

@ -0,0 +1,62 @@
using System;
using System.IO;
using System.Security.AccessControl;
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
using API.Entities;
using API.Logging;
using API.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Core;
using ILogger = Serilog.ILogger;
namespace API.Middleware;
public class SecurityEventMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public SecurityEventMiddleware(RequestDelegate next)
{
_next = next;
_logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(Path.Join(Directory.GetCurrentDirectory(), "config/logs/", "security.log"), rollingInterval: RollingInterval.Day)
.CreateLogger();
}
public async Task InvokeAsync(HttpContext context)
{
var ipAddress = context.Connection.RemoteIpAddress?.ToString();
var requestMethod = context.Request.Method;
var requestPath = context.Request.Path;
var userAgent = context.Request.Headers["User-Agent"];
var securityEvent = new SecurityEvent
{
IpAddress = ipAddress,
RequestMethod = requestMethod,
RequestPath = requestPath,
UserAgent = userAgent,
CreatedAt = DateTime.Now,
CreatedAtUtc = DateTime.UtcNow,
};
using (var scope = context.RequestServices.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
dbContext.Add(securityEvent);
await dbContext.SaveChangesAsync();
_logger.Debug("Request Processed: {@SecurityEvent}", securityEvent);
}
await _next(context);
}
}

View File

@ -76,7 +76,8 @@ public class BookmarkService : IBookmarkService
/// <summary>
/// This is a job that runs after a bookmark is saved
/// </summary>
private async Task ConvertBookmarkToWebP(int bookmarkId)
/// <remarks>This must be public</remarks>
public async Task ConvertBookmarkToWebP(int bookmarkId)
{
var bookmarkDirectory =
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;

View File

@ -32,6 +32,7 @@ public interface ICacheService
void CleanupChapters(IEnumerable<int> chapterIds);
void CleanupBookmarks(IEnumerable<int> seriesIds);
string GetCachedPagePath(int chapterId, int page);
IEnumerable<string> GetCachedPages(int chapterId);
IEnumerable<FileDimensionDto> GetCachedFileDimensions(int chapterId);
string GetCachedBookmarkPagePath(int seriesId, int page);
string GetCachedFile(Chapter chapter);
@ -58,6 +59,13 @@ public class CacheService : ICacheService
_bookmarkService = bookmarkService;
}
public IEnumerable<string> GetCachedPages(int chapterId)
{
var path = GetCachePath(chapterId);
return _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions)
.OrderByNatural(Path.GetFileNameWithoutExtension);
}
public IEnumerable<FileDimensionDto> GetCachedFileDimensions(int chapterId)
{
var sw = Stopwatch.StartNew();
@ -276,15 +284,7 @@ public class CacheService : ICacheService
.OrderByNatural(Path.GetFileNameWithoutExtension)
.ToArray();
if (files.Length == 0)
{
return string.Empty;
}
if (page > files.Length) page = files.Length;
// Since array is 0 based, we need to keep that in account (only affects last image)
return page == files.Length ? files.ElementAt(page - 1) : files.ElementAt(page);
return GetPageFromFiles(files, page);
}
public async Task<int> CacheBookmarkForSeries(int userId, int seriesId)
@ -310,4 +310,33 @@ public class CacheService : ICacheService
_directoryService.ClearAndDeleteDirectory(destDirectory);
}
/// <summary>
/// Returns either the file or an empty string
/// </summary>
/// <param name="files"></param>
/// <param name="pageNum"></param>
/// <returns></returns>
public static string GetPageFromFiles(string[] files, int pageNum)
{
files = files
.AsEnumerable()
.OrderByNatural(Path.GetFileNameWithoutExtension)
.ToArray();
if (files.Length == 0)
{
return string.Empty;
}
if (pageNum < 0)
{
pageNum = 0;
}
// Since array is 0 based, we need to keep that in account (only affects last image)
return pageNum >= files.Length ? files.ElementAt(Math.Min(pageNum - 1, files.Length - 1)) : files.ElementAt(pageNum);
}
}

View File

@ -22,9 +22,25 @@ public interface IImageService
/// <param name="thumbnailWidth">Width of thumbnail</param>
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = 320);
/// <summary>
/// Writes out a thumbnail by stream input
/// </summary>
/// <param name="stream"></param>
/// <param name="fileName"></param>
/// <param name="outputDirectory"></param>
/// <param name="saveAsWebP"></param>
/// <returns></returns>
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false);
/// <summary>
/// Writes out a thumbnail by file path input
/// </summary>
/// <param name="sourceFile"></param>
/// <param name="fileName"></param>
/// <param name="outputDirectory"></param>
/// <param name="saveAsWebP"></param>
/// <returns></returns>
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false);
/// <summary>
/// Converts the passed image to webP and outputs it in the same directory
/// </summary>
/// <param name="filePath">Full path to the image to convert</param>
@ -115,6 +131,19 @@ public class ImageService : IImageService
return filename;
}
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false)
{
using var thumbnail = Image.Thumbnail(sourceFile, ThumbnailWidth);
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
_directoryService.ExistOrCreate(outputDirectory);
try
{
_directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
} catch (Exception) {/* Swallow exception */}
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
return filename;
}
public Task<string> ConvertToWebP(string filePath, string outputPath)
{
var file = _directoryService.FileSystem.FileInfo.New(filePath);
@ -218,6 +247,16 @@ public class ImageService : IImageService
return $"readinglist{readingListId}";
}
/// <summary>
/// Returns the name format for a thumbnail (temp thumbnail)
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public static string GetThumbnailFormat(int chapterId)
{
return $"thumbnail{chapterId}";
}
public static string CreateMergedImage(List<string> coverImages, string dest)
{

View File

@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -33,6 +34,7 @@ public interface IReaderService
Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber);
HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub);
IDictionary<int, int> GetPairs(IEnumerable<FileDimensionDto> dimensions);
Task<string> GetThumbnail(Chapter chapter, int pageNum, IEnumerable<string> cachedImages);
}
public class ReaderService : IReaderService
@ -40,6 +42,8 @@ public class ReaderService : IReaderService
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ReaderService> _logger;
private readonly IEventHub _eventHub;
private readonly IImageService _imageService;
private readonly IDirectoryService _directoryService;
private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerZeroFirst.Default;
@ -48,14 +52,17 @@ public class ReaderService : IReaderService
public const float AvgWordsPerHour = (MaxWordsPerHour + MinWordsPerHour) / 2F;
private const float MinPagesPerMinute = 3.33F;
private const float MaxPagesPerMinute = 2.75F;
public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F;
public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F; //3.04
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IEventHub eventHub)
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IEventHub eventHub, IImageService imageService,
IDirectoryService directoryService)
{
_unitOfWork = unitOfWork;
_logger = logger;
_eventHub = eventHub;
_imageService = imageService;
_directoryService = directoryService;
}
public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId)
@ -644,6 +651,44 @@ public class ReaderService : IReaderService
return pairs;
}
/// <summary>
///
/// </summary>
/// <param name="chapter"></param>
/// <param name="pageNum"></param>
/// <param name="cachedImages"></param>
/// <returns>Full path of thumbnail</returns>
public async Task<string> GetThumbnail(Chapter chapter, int pageNum, IEnumerable<string> cachedImages)
{
var outputDirectory =
_directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, ImageService.GetThumbnailFormat(chapter.Id));
try
{
var saveAsWebp =
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
if (!Directory.Exists(outputDirectory))
{
var outputtedThumbnails = cachedImages
.Select((img, idx) =>
_directoryService.FileSystem.Path.Join(outputDirectory,
_imageService.WriteCoverThumbnail(img, $"{idx}", outputDirectory, saveAsWebp)))
.ToArray();
return CacheService.GetPageFromFiles(outputtedThumbnails, pageNum);
}
var files = _directoryService.GetFilesWithExtension(outputDirectory,
Tasks.Scanner.Parser.Parser.ImageFileExtensions);
return CacheService.GetPageFromFiles(files, pageNum);
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an error when trying to get thumbnail for Chapter {ChapterId}, Page {PageNum}", chapter.Id, pageNum);
_directoryService.ClearAndDeleteDirectory(outputDirectory);
throw;
}
}
/// <summary>
/// Formats a Chapter name based on the library it's in
/// </summary>
@ -668,4 +713,6 @@ public class ReaderService : IReaderService
throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null);
}
}
}

View File

@ -509,7 +509,7 @@ public class ReadingListService : IReadingListService
var allReadingLists = (user.ReadingLists).ToDictionary(s => s.NormalizedTitle);
if (!allReadingLists.TryGetValue(readingListNameNormalized, out var readingList))
{
readingList = DbFactory.ReadingList(cblReading.Name, string.Empty, false);
readingList = DbFactory.ReadingList(cblReading.Name, cblReading.Summary, false);
user.ReadingLists.Add(readingList);
}
else

View File

@ -103,7 +103,6 @@ public class StatisticService : IStatisticService
})
.ToListAsync();
var averageReadingTimePerWeek = _context.AppUserProgresses
.Where(p => p.AppUserId == userId)
.Join(_context.Chapter, p => p.ChapterId, c => c.Id,
@ -112,7 +111,7 @@ public class StatisticService : IStatisticService
AverageReadingHours = Math.Min((float) p.PagesRead / (float) c.Pages, 1.0) * ((float) c.AvgHoursToRead)
})
.Select(x => x.AverageReadingHours)
.Average() / 7.0;
.Average() * 7.0;
return new UserReadStatistics()
{

View File

@ -58,6 +58,17 @@ public class CleanupService : ICleanupService
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
public async Task Cleanup()
{
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
TaskScheduler.DefaultQueue, true) ||
TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToWebP", Array.Empty<object>(),
TaskScheduler.DefaultQueue, true))
{
_logger.LogInformation("Cleanup put on hold as a conversion to WebP in progress");
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a conversion to WebP in progress"));
return;
}
_logger.LogInformation("Starting Cleanup");
await SendProgress(0F, "Starting cleanup");
_logger.LogInformation("Cleaning temp directory");
@ -90,6 +101,7 @@ public class CleanupService : ICleanupService
await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated();
await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated();
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
await _unitOfWork.ReadingListRepository.RemoveReadingListsWithoutSeries();
}
private async Task SendProgress(float progress, string subtitle)

View File

@ -225,6 +225,11 @@ public static class Parser
new Regex(
@"(?<Series>.+?):? (\b|_|-)(vol)\.?(\s|-|_)?\d+",
MatchOptions, RegexTimeout),
// [xPearse] Kyochuu Rettou Chapter 001 Volume 1 [English] [Manga] [Volume Scans]
new Regex(
@"(?<Series>.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)",
MatchOptions,
RegexTimeout),
// [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]
new Regex(
@"(?<Series>.+?):? (\b|_|-)(vol)(ume)",

View File

@ -51,6 +51,9 @@ public class ProcessSeries : IProcessSeries
private IList<Person> _people;
private Dictionary<string, Tag> _tags;
private Dictionary<string, CollectionTag> _collectionTags;
private readonly object _peopleLock;
private readonly object _genreLock;
private readonly object _tagLock;
public ProcessSeries(IUnitOfWork unitOfWork, ILogger<ProcessSeries> logger, IEventHub eventHub,
IDirectoryService directoryService, ICacheHelper cacheHelper, IReadingItemService readingItemService,
@ -838,7 +841,7 @@ public class ProcessSeries : IProcessSeries
if (person == null)
{
person = DbFactory.Person(name, role);
lock (_people)
lock (_peopleLock)
{
_people.Add(person);
}
@ -865,7 +868,7 @@ public class ProcessSeries : IProcessSeries
if (newTag)
{
genre = DbFactory.Genre(name);
lock (_genres)
lock (_genreLock)
{
_genres.Add(normalizedName, genre);
_unitOfWork.GenreRepository.Attach(genre);
@ -894,7 +897,7 @@ public class ProcessSeries : IProcessSeries
if (tag == null)
{
tag = DbFactory.Tag(name);
lock (_tags)
lock (_tagLock)
{
_tags.Add(normalizedName, tag);
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Threading.RateLimiting;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
@ -14,6 +15,7 @@ using API.Entities.Enums;
using API.Extensions;
using API.Logging;
using API.Middleware;
using API.Middleware.RateLimit;
using API.Services;
using API.Services.HostedServices;
using API.Services.Tasks;
@ -179,6 +181,19 @@ public class Startup
services.AddResponseCaching();
services.AddRateLimiter(options =>
{
options.AddPolicy("Authentication", httpContext =>
new AuthenticationRateLimiterPolicy().GetPartition(httpContext));
// RateLimitPartition.GetFixedWindowLimiter(httpContext.Connection.RemoteIpAddress?.ToString(),
// partition => new FixedWindowRateLimiterOptions
// {
// AutoReplenishment = true,
// PermitLimit = 1,
// Window = TimeSpan.FromMinutes(1),
// }));
});
services.AddHangfire(configuration => configuration
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
@ -259,6 +274,7 @@ public class Startup
app.UseMiddleware<ExceptionMiddleware>();
app.UseMiddleware<SecurityEventMiddleware>();
if (env.IsDevelopment())
{
@ -278,10 +294,16 @@ public class Startup
app.UseForwardedHeaders();
var basePath = Configuration.BaseUrl;
app.UseRateLimiter();
var basePath = Configuration.BaseUrl;
app.UsePathBase(basePath);
UpdateBaseUrlInIndex(basePath);
if (!env.IsDevelopment())
{
// We don't update the index.html in local as we don't serve from there
UpdateBaseUrlInIndex(basePath);
}
app.UseRouting();
@ -292,7 +314,17 @@ public class Startup
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials() // For SignalR token query param
.WithOrigins("http://localhost:4200", $"http://{GetLocalIpAddress()}:4200", $"http://{GetLocalIpAddress()}:5000")
.WithOrigins("http://localhost:4200", $"http://{GetLocalIpAddress()}:4200", $"http://{GetLocalIpAddress()}:5000", "https://kavita.majora2007.duckdns.org")
.WithExposedHeaders("Content-Disposition", "Pagination"));
}
else
{
// Allow CORS for Kavita's url
app.UseCors(policy => policy
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials() // For SignalR token query param
.WithOrigins("https://kavita.majora2007.duckdns.org")
.WithExposedHeaders("Content-Disposition", "Pagination"));
}
@ -311,6 +343,7 @@ public class Startup
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + TimeSpan.FromHours(24);
ctx.Context.Response.Headers["X-Robots-Tag"] = "noindex,nofollow";
}
});
@ -326,7 +359,7 @@ public class Startup
new[] { "Accept-Encoding" };
// Don't let the site be iframed outside the same origin (clickjacking)
context.Response.Headers.XFrameOptions = "SAMEORIGIN";
context.Response.Headers.XFrameOptions = Configuration.XFrameOptions;
// Setup CSP to ensure we load assets only from these origins
context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';");
@ -359,19 +392,26 @@ public class Startup
});
var _logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
_logger.LogInformation("Starting with base url as {baseUrl}", basePath);
_logger.LogInformation("Starting with base url as {BaseUrl}", basePath);
}
private static void UpdateBaseUrlInIndex(string baseUrl)
{
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker) return;
var htmlDoc = new HtmlDocument();
var indexHtmlPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html");
htmlDoc.Load(indexHtmlPath);
try
{
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker) return;
var htmlDoc = new HtmlDocument();
var indexHtmlPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html");
htmlDoc.Load(indexHtmlPath);
var baseNode = htmlDoc.DocumentNode.SelectSingleNode("/html/head/base");
baseNode.SetAttributeValue("href", baseUrl);
htmlDoc.Save(indexHtmlPath);
var baseNode = htmlDoc.DocumentNode.SelectSingleNode("/html/head/base");
baseNode.SetAttributeValue("href", baseUrl);
htmlDoc.Save(indexHtmlPath);
}
catch (Exception ex)
{
Log.Error(ex, "There was an error setting base url");
}
}
private static void OnShutdown()

View File

@ -10,6 +10,7 @@ public static class Configuration
{
public const string DefaultIpAddresses = "0.0.0.0,::";
public const string DefaultBaseUrl = "/";
public const string DefaultXFrameOptions = "SAMEORIGIN";
private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
public static int Port
@ -36,6 +37,8 @@ public static class Configuration
set => SetBaseUrl(GetAppSettingFilename(), value);
}
public static string XFrameOptions => GetXFrameOptions(GetAppSettingFilename());
private static string GetAppSettingFilename()
{
if (!string.IsNullOrEmpty(AppSettingsFilename))
@ -224,7 +227,7 @@ public static class Configuration
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{
var baseUrl = tokenElement.GetString();
if (!String.IsNullOrEmpty(baseUrl))
if (!string.IsNullOrEmpty(baseUrl))
{
baseUrl = !baseUrl.StartsWith("/")
? $"/{baseUrl}"
@ -277,6 +280,35 @@ public static class Configuration
}
#endregion
#region XFrameOrigins
private static string GetXFrameOptions(string filePath)
{
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
{
return DefaultBaseUrl;
}
try
{
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
const string key = "XFrameOrigins";
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{
var origins = tokenElement.GetString();
return !string.IsNullOrEmpty(origins) ? origins : DefaultBaseUrl;
}
}
catch (Exception ex)
{
Console.WriteLine("Error reading app settings: " + ex.Message);
}
return DefaultXFrameOptions;
}
#endregion
private class AppSettings
{
public string TokenKey { get; set; }

View File

@ -14,7 +14,7 @@
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.53.0.62665">
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.54.0.64047">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -34,7 +34,6 @@
"file-saver": "^2.0.5",
"lazysizes": "^5.3.2",
"ng-circle-progress": "^1.7.1",
"ng-swipe": "^2.0.1",
"ngx-color-picker": "^13.0.0",
"ngx-extended-pdf-viewer": "^15.2.2",
"ngx-file-drop": "^14.0.2",
@ -5620,14 +5619,6 @@
"node": ">=8.9.0"
}
},
"node_modules/ag-swipe-core": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ag-swipe-core/-/ag-swipe-core-1.0.2.tgz",
"integrity": "sha512-NNONbrEbsmu6wsl7E07eGYVZw8Wx7hOok2TlhQLU/50EUhmI3Vpg8EDz0rWhV/HrfUAoEd4LxBvLAeT9DswQDw==",
"dependencies": {
"rxjs": "^7.5.5"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -13906,19 +13897,6 @@
"rxjs": ">=6.4.0"
}
},
"node_modules/ng-swipe": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ng-swipe/-/ng-swipe-2.0.1.tgz",
"integrity": "sha512-y4w2d719VK1u6KUlNqhHVevzT+yR30bnTTLkFNEsVG3Gp5+oZhUnflVNWfzIw+O8GCjZqVLelwla/jOkqUclmQ==",
"dependencies": {
"ag-swipe-core": "^1.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^14.0.0",
"@angular/core": "^14.0.0"
}
},
"node_modules/ngx-color-picker": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-13.0.0.tgz",
@ -22337,14 +22315,6 @@
}
}
},
"ag-swipe-core": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ag-swipe-core/-/ag-swipe-core-1.0.2.tgz",
"integrity": "sha512-NNONbrEbsmu6wsl7E07eGYVZw8Wx7hOok2TlhQLU/50EUhmI3Vpg8EDz0rWhV/HrfUAoEd4LxBvLAeT9DswQDw==",
"requires": {
"rxjs": "^7.5.5"
}
},
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -28587,15 +28557,6 @@
"tslib": "^2.0.0"
}
},
"ng-swipe": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ng-swipe/-/ng-swipe-2.0.1.tgz",
"integrity": "sha512-y4w2d719VK1u6KUlNqhHVevzT+yR30bnTTLkFNEsVG3Gp5+oZhUnflVNWfzIw+O8GCjZqVLelwla/jOkqUclmQ==",
"requires": {
"ag-swipe-core": "^1.0.0",
"tslib": "^2.3.0"
}
},
"ngx-color-picker": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-13.0.0.tgz",

View File

@ -40,7 +40,6 @@
"file-saver": "^2.0.5",
"lazysizes": "^5.3.2",
"ng-circle-progress": "^1.7.1",
"ng-swipe": "^2.0.1",
"ngx-color-picker": "^13.0.0",
"ngx-extended-pdf-viewer": "^15.2.2",
"ngx-file-drop": "^14.0.2",

View File

@ -101,6 +101,10 @@ export class ReaderService {
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
}
getThumbnailUrl(chapterId: number, page: number) {
return this.baseUrl + 'reader/thumbnail?chapterId=' + chapterId + '&page=' + page;
}
getBookmarkPageUrl(seriesId: number, apiKey: string, page: number) {
return this.baseUrl + 'reader/bookmark-image?seriesId=' + seriesId + '&page=' + page + '&apiKey=' + encodeURIComponent(apiKey);
}

View File

@ -32,7 +32,7 @@
</div>
<div class="d-flex align-items-center justify-content-between text-center row g-0">
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<div class="col-1">{{pageNum}}</div>
<div class="col-1" (click)="goToPage(0)">{{pageNum}}</div>
<div class="col-8">
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
</div>

View File

@ -379,10 +379,12 @@
</div>
<div class="row g-0 mb-2" *ngIf="metadata">
<div class="col-md-6">
Max Items: {{metadata.maxCount}} <i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Max of Volume/Issue field in ComicInfo. Used in conjunction with total items to determine publication status." role="button" tabindex="0"></i>
Max Items: {{metadata.maxCount}}
<i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Max of Volume/Issue field in ComicInfo. Used in conjunction with total items to determine publication status." role="button" tabindex="0"></i>
</div>
<div class="col-md-6" title="">
Total Items: {{metadata.totalCount}} <i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Total number of issues/volumes in the series" role="button" tabindex="0"></i>
<div class="col-md-6">
Total Items: {{metadata.totalCount}}
<i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Total number of issues/volumes in the series" role="button" tabindex="0"></i>
</div>
<div class="col-md-6">Publication Status: {{metadata.publicationStatus | publicationStatus}}</div>
<div class="col-md-6">Total Pages: {{series.pages}}</div>

View File

@ -278,6 +278,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter));
}
this.collectionTagSettings.compareFnForAdd = (options: CollectionTag[], filter: string) => {
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
}
this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => {
return a.title === b.title;
}
@ -310,6 +313,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => {
return a.id == b.id;
}
this.tagsSettings.compareFnForAdd = (options: Tag[], filter: string) => {
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
}
if (this.metadata.tags) {
this.tagsSettings.savedData = this.metadata.tags;
@ -331,6 +337,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter));
}
this.genreSettings.compareFnForAdd = (options: Genre[], filter: string) => {
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
}
this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => {
return a.title == b.title;
}
@ -372,6 +381,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.languageSettings.compareFn = (options: Language[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter));
}
this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => {
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
}
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
@ -426,6 +438,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
personSettings.compareFn = (options: Person[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.name, filter));
}
personSettings.compareFnForAdd = (options: Person[], filter: string) => {
return options.filter(m => this.utilityService.filterMatches(m.name, filter));
}
personSettings.selectionCompareFn = (a: Person, b: Person) => {
return a.name == b.name && a.role == b.role;

View File

@ -1,6 +1,8 @@
@use '../../../../manga-reader-common';
.image-container {
height: calc(100vh); // override as on single, we -34px for the potential scrollbar
#image-1 {
&.double {
margin: 0 0 0 auto;

View File

@ -2,6 +2,8 @@
// Overrides for reverse
.image-container {
height: calc(100vh); // override as on single, we -34px for the potential scrollbar
&.reverse {
overflow: unset;
display: flex;

View File

@ -31,8 +31,8 @@ import { DoubleRendererComponent } from '../double-renderer/double-renderer.comp
import { DoubleReverseRendererComponent } from '../double-reverse-renderer/double-reverse-renderer.component';
import { SingleRendererComponent } from '../single-renderer/single-renderer.component';
import { ChapterInfo } from '../../_models/chapter-info';
import { SwipeEvent } from 'ng-swipe';
import { DoubleNoCoverRendererComponent } from '../double-renderer-no-cover/double-no-cover-renderer.component';
import { SwipeEvent } from 'src/app/ng-swipe/ag-swipe.core';
const PREFETCH_PAGES = 10;
@ -379,7 +379,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// This is for the pagination area
get MaxHeight() {
if (this.FittingOption !== FITTING_OPTION.HEIGHT) {
return this.mangaReaderService.getPageDimensions(this.pageNum)?.height + 'px';
return Math.min(this.readingArea?.nativeElement?.clientHeight, this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) + 'px';
}
return 'calc(var(--vh) * 100)';
}

View File

@ -17,8 +17,8 @@ import { DoubleRendererComponent } from './_components/double-renderer/double-re
import { DoubleReverseRendererComponent } from './_components/double-reverse-renderer/double-reverse-renderer.component';
import { MangaReaderComponent } from './_components/manga-reader/manga-reader.component';
import { FittingIconPipe } from './_pipes/fitting-icon.pipe';
import { SwipeModule } from 'ng-swipe';
import { DoubleNoCoverRendererComponent } from './_components/double-renderer-no-cover/double-no-cover-renderer.component';
import { NgSwipeModule } from '../ng-swipe/ng-swipe.module';
@NgModule({
declarations: [
@ -45,7 +45,7 @@ import { DoubleNoCoverRendererComponent } from './_components/double-renderer-no
SharedModule,
ReaderSharedModule,
SwipeModule
NgSwipeModule
],
exports: [
MangaReaderComponent

View File

@ -329,6 +329,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.ageRatingSettings.compareFn = (options: AgeRatingDto[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter));
}
this.ageRatingSettings.selectionCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => {
return a.title == b.title;

View File

@ -0,0 +1,103 @@
import { fromEvent, Observable, race, Subscription } from 'rxjs';
import { elementAt, map, switchMap, takeUntil, tap } from 'rxjs/operators';
export interface SwipeCoordinates {
x: number;
y: number;
}
export enum SwipeDirection {
X = 'x',
Y = 'y'
}
export interface SwipeStartEvent {
x: number;
y: number;
direction: SwipeDirection;
}
export interface SwipeEvent {
direction: SwipeDirection;
distance: number;
}
export interface SwipeSubscriptionConfig {
domElement: HTMLElement;
onSwipeMove?: (event: SwipeEvent) => void;
onSwipeEnd?: (event: SwipeEvent) => void;
}
export function createSwipeSubscription({ domElement, onSwipeMove, onSwipeEnd }: SwipeSubscriptionConfig): Subscription {
if (!(domElement instanceof HTMLElement)) {
throw new Error('Provided domElement should be an instance of HTMLElement');
}
if ((typeof onSwipeMove !== 'function') && (typeof onSwipeEnd !== 'function')) {
throw new Error('At least one of the following swipe event handler functions should be provided: onSwipeMove and/or onSwipeEnd');
}
const touchStarts$ = fromEvent<TouchEvent>(domElement, 'touchstart').pipe(map(getTouchCoordinates));
const touchMoves$ = fromEvent<TouchEvent>(domElement, 'touchmove').pipe(map(getTouchCoordinates));
const touchEnds$ = fromEvent<TouchEvent>(domElement, 'touchend').pipe(map(getTouchCoordinates));
const touchCancels$ = fromEvent<TouchEvent>(domElement, 'touchcancel');
const touchStartsWithDirection$: Observable<SwipeStartEvent> = touchStarts$.pipe(
switchMap((touchStartEvent: SwipeCoordinates) => touchMoves$.pipe(
elementAt(3),
map((touchMoveEvent: SwipeCoordinates) => ({
x: touchStartEvent.x,
y: touchStartEvent.y,
direction: getTouchDirection(touchStartEvent, touchMoveEvent)
})
))
)
);
return touchStartsWithDirection$.pipe(
switchMap(touchStartEvent => touchMoves$.pipe(
map(touchMoveEvent => getTouchDistance(touchStartEvent, touchMoveEvent)),
tap((coordinates: SwipeCoordinates) => {
if (typeof onSwipeMove !== 'function') { return; }
onSwipeMove(getSwipeEvent(touchStartEvent, coordinates));
}),
takeUntil(race(
touchEnds$.pipe(
map(touchEndEvent => getTouchDistance(touchStartEvent, touchEndEvent)),
tap((coordinates: SwipeCoordinates) => {
if (typeof onSwipeEnd !== 'function') { return; }
onSwipeEnd(getSwipeEvent(touchStartEvent, coordinates));
})
),
touchCancels$
))
))
).subscribe();
}
function getTouchCoordinates(touchEvent: TouchEvent): SwipeCoordinates {
return {
x: touchEvent.changedTouches[0].clientX,
y: touchEvent.changedTouches[0].clientY
};
}
function getTouchDistance(startCoordinates: SwipeCoordinates, moveCoordinates: SwipeCoordinates): SwipeCoordinates {
return {
x: moveCoordinates.x - startCoordinates.x,
y: moveCoordinates.y - startCoordinates.y
};
}
function getTouchDirection(startCoordinates: SwipeCoordinates, moveCoordinates: SwipeCoordinates): SwipeDirection {
const { x, y } = getTouchDistance(startCoordinates, moveCoordinates);
return Math.abs(x) < Math.abs(y) ? SwipeDirection.Y : SwipeDirection.X;
}
function getSwipeEvent(touchStartEvent: SwipeStartEvent, coordinates: SwipeCoordinates): SwipeEvent {
return {
direction: touchStartEvent.direction,
distance: coordinates[touchStartEvent.direction]
};
}

View File

@ -0,0 +1,32 @@
import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { createSwipeSubscription, SwipeEvent } from './ag-swipe.core';
@Directive({
selector: '[ngSwipe]'
})
export class SwipeDirective implements OnInit, OnDestroy {
private swipeSubscription: Subscription | undefined;
@Output() swipeMove: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
@Output() swipeEnd: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
constructor(
private elementRef: ElementRef,
private zone: NgZone
) {}
ngOnInit() {
this.zone.runOutsideAngular(() => {
this.swipeSubscription = createSwipeSubscription({
domElement: this.elementRef.nativeElement,
onSwipeMove: (swipeMoveEvent: SwipeEvent) => this.swipeMove.emit(swipeMoveEvent),
onSwipeEnd: (swipeEndEvent: SwipeEvent) => this.swipeEnd.emit(swipeEndEvent)
});
});
}
ngOnDestroy() {
this.swipeSubscription?.unsubscribe?.();
}
}

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SwipeDirective } from './ng-swipe.directive';
// All code in this module is based on https://github.com/aGoncharuks/ag-swipe and may contain further enhancements or bugfixes.
@NgModule({
declarations: [
SwipeDirective
],
imports: [
CommonModule
],
exports: [
SwipeDirective
]
})
export class NgSwipeModule { }

View File

@ -13,7 +13,7 @@ export class TimeDurationPipe implements PipeTransform {
if (hours < 1) {
return `${(hours * 60).toFixed(1)} minutes`;
} else if (hours < 24) {
return `${hours} hours`;
return `${hours.toFixed(1)} hours`;
} else if (hours < 720) {
return `${(hours / 24).toFixed(1)} days`;
} else if (hours < 8760) {

View File

@ -92,11 +92,17 @@ export class UtilityService {
}
filter(input: string, filter: string): boolean {
if (input === null || filter === null) return false;
if (input === null || filter === null || input === undefined || filter === undefined) return false;
const reg = /[_\.\-]/gi;
return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, ''));
}
filterMatches(input: string, filter: string): boolean {
if (input === null || filter === null || input === undefined || filter === undefined) return false;
const reg = /[_\.\-]/gi;
return input.toUpperCase().replace(reg, '') === filter.toUpperCase().replace(reg, '');
}
isVolume(d: any) {
return d != null && d.hasOwnProperty('chapters');
}

View File

@ -29,7 +29,7 @@
<ng-container>
<div class="col-auto mb-2">
<app-icon-and-title label="Average Reading / Week" [clickable]="false" fontClasses="fas fa-eye">
{{avgHoursPerWeekSpentReading | timeDuration}}
{{avgHoursPerWeekSpentReading | timeDuration}}
</app-icon-and-title>
</div>
<div class="vr d-none d-lg-block m-2"></div>

View File

@ -236,16 +236,16 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
this.filteredOptions = this.typeaheadForm.get('typeahead')!.valueChanges
.pipe(
// Adjust input box to grow
tap(val => {
tap((val: string) => {
if (this.inputElem != null && this.inputElem.nativeElement != null) {
this.renderer2.setStyle(this.inputElem.nativeElement, 'width', 15 * (val.trim().length + 1) + 'px');
this.focusedIndex = 0;
}
}),
map(val => val.trim()),
map((val: string) => val.trim()),
auditTime(this.settings.debounce),
//distinctUntilChanged(), // ?!: BUG Doesn't trigger the search to run when filtered array changes
filter(val => {
filter((val: string) => {
// If minimum filter characters not met, do not filter
if (this.settings.minCharacters === 0) return true;
@ -256,11 +256,11 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
return true;
}),
switchMap(val => {
switchMap((val: string) => {
this.isLoadingOptions = true;
return this.settings.fetchFn(val.trim()).pipe(takeUntil(this.onDestroy), map((items: any[]) => items.filter(item => this.filterSelected(item))));
}),
tap((filteredOptions) => {
tap((filteredOptions: any[]) => {
this.isLoadingOptions = false;
this.focusedIndex = 0;
this.cdRef.markForCheck();
@ -398,6 +398,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
}
this.toggleSelection(opt);
console.log('Selected ', opt);
this.resetField();
this.onInputFocus();
@ -410,6 +411,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
const newItem = this.settings.addTransformFn(title);
this.newItemAdded.emit(newItem);
this.toggleSelection(newItem);
console.log('Selected ', newItem);
this.resetField();
this.onInputFocus();
@ -482,14 +484,43 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
updateShowAddItem(options: any[]) {
// ?! BUG This will still technicially allow you to add the same thing as a previously added item. (Code will just toggle it though)
this.showAddItem = this.settings.addIfNonExisting && this.typeaheadControl.value.trim()
&& this.typeaheadControl.value.trim().length >= Math.max(this.settings.minCharacters, 1)
&& this.typeaheadControl.dirty
&& (typeof this.settings.compareFn == 'function' && this.settings.compareFn(options, this.typeaheadControl.value.trim()).length === 0);
this.showAddItem = false;
this.cdRef.markForCheck();
if (!this.settings.addIfNonExisting) return;
const inputText = this.typeaheadControl.value.trim();
if (inputText.length < Math.max(this.settings.minCharacters, 1)) return;
if (!this.typeaheadControl.dirty) return; // Do we need this?
// Check if this new option will interfere with any existing ones not shown
if (typeof this.settings.compareFnForAdd == 'function') {
console.log('filtered options: ', this.optionSelection.selected());
const willDuplicateExist = this.settings.compareFnForAdd(this.optionSelection.selected(), inputText);
console.log('duplicate check: ', willDuplicateExist);
if (willDuplicateExist.length > 0) {
console.log("can't show add, duplicates will exist");
return;
}
}
if (typeof this.settings.compareFn == 'function') {
// The problem here is that compareFn can report that duplicate will exist as it does contains not match
const matches = this.settings.compareFn(options, inputText);
console.log('matches for ', inputText, ': ', matches);
console.log('matches include input string: ', matches.includes(this.settings.addTransformFn(inputText)));
if (matches.length > 0 && matches.includes(this.settings.addTransformFn(inputText))) {
console.log("can't show add, there are still ");
return;
}
}
this.showAddItem = true;
if (this.showAddItem) {
this.hasFocus = true;
}
this.cdRef.markForCheck();
}
toggleLock(event: any) {

View File

@ -29,9 +29,13 @@ export class TypeaheadSettings<T> {
savedData!: T[] | T;
/**
* Function to compare the elements. Should return all elements that fit the matching criteria.
* This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead (TODO)
* This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead.
*/
compareFn!: ((optionList: T[], filter: string) => T[]);
/**
* Must be defined when addIfNonExisting is true. Used to ensure no duplicates exist when adding.
*/
compareFnForAdd!: ((optionList: T[], filter: string) => T[]);
/**
* Function which is used for comparing objects when keeping track of state.
* Useful over shallow equal when you have image urls that have random numbers on them.

View File

@ -9,7 +9,7 @@
</div>
<p *ngIf="isAdmin">
Looking for a light or e-ink theme? We have some custom themes you can use on our <a href="https://wiki.kavitareader.com/en/guides/settings/themes" target="_blank" rel="noopener noreferrer">wiki</a>.
Looking for a light or e-ink theme? We have some custom themes you can use on our <a href="https://github.com/Kareadita/Themes" target="_blank" rel="noopener noreferrer">theme github</a>.
</p>
<div class="row g-0">

View File

@ -14,8 +14,8 @@
<meta name="msapplication-config" content="assets/icons/browserconfig.xml">
<meta name="theme-color" content="#000000">
<meta name="apple-mobile-web-app-status-bar-style" content="#000000">
<meta name='robots' content='noindex,follow' />
<!-- Don't allow indexing from Bots -->
<meta name='robots' content='none' />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">

View File

@ -24,10 +24,10 @@ input:not([type="range"]), .form-control {
}
}
.form-range::-webkit-slider-thumb:active {
.form-range::-webkit-slider-thumb:active, .form-range::-moz-range-thumb:active {
background-color: var(--input-range-active-color);
}
.form-range::-webkit-slider-thumb {
.form-range::-webkit-slider-thumb, .form-range::-moz-range-thumb {
background-color: var(--input-range-color);
}

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.1.20"
"version": "0.7.1.22"
},
"servers": [
{
@ -3631,6 +3631,36 @@
}
}
},
"/api/Reader/thumbnail": {
"get": {
"tags": [
"Reader"
],
"parameters": [
{
"name": "chapterId",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "pageNum",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/Reader/bookmark-image": {
"get": {
"tags": [
@ -3739,6 +3769,7 @@
"Reader"
],
"summary": "Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.",
"description": "This is generally the first call when attempting to read to allow pre-generation of assets needed for reading",
"parameters": [
{
"name": "chapterId",