OPDS Cleanup (#534)

* Fixed opds url display

* Rewrote how stat collection works, now we check in multiple places and always run stat collection in a background thread, to not block main thread.

* Cleaned up the ParseInfoTest to be more verbose

* Added benchmarking
This commit is contained in:
Joseph Milazzo 2021-08-28 15:32:24 -07:00 committed by GitHub
parent d36c3d62ce
commit 51b9d1a45a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 133 additions and 52 deletions

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\API\API.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.1" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,40 @@
using System;
using System.IO;
using API.Entities.Enums;
using API.Interfaces.Services;
using API.Services;
using API.Services.Tasks.Scanner;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using Microsoft.Extensions.Logging;
using NSubstitute;
namespace API.Benchmark
{
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
[SimpleJob(launchCount: 1, warmupCount: 3, targetCount: 5, invocationCount: 100, id: "Test"), ShortRunJob]
public class ParseScannedFilesBenchmarks
{
private readonly ParseScannedFiles _parseScannedFiles;
private readonly ILogger<ParseScannedFiles> _logger = Substitute.For<ILogger<ParseScannedFiles>>();
private readonly ILogger<BookService> _bookLogger = Substitute.For<ILogger<BookService>>();
public ParseScannedFilesBenchmarks()
{
IBookService bookService = new BookService(_bookLogger);
_parseScannedFiles = new ParseScannedFiles(bookService, _logger);
}
[Benchmark]
public void Test()
{
var libraryPath = Path.Join(Directory.GetCurrentDirectory(),
"../../../Services/Test Data/ScannerService/Manga");
var parsedSeries = _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new string[] {libraryPath},
out var totalFiles, out var scanElapsedTime);
}
}
}

18
API.Benchmark/Program.cs Normal file
View File

@ -0,0 +1,18 @@
using BenchmarkDotNet.Running;
namespace API.Benchmark
{
/// <summary>
/// To build this, cd into API.Benchmark directory and run
/// dotnet build -c Release
/// then copy the outputted dll
/// dotnet copied_string\API.Benchmark.dll
/// </summary>
public class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<ParseScannedFilesBenchmarks>();
}
}
}

View File

@ -312,14 +312,14 @@ namespace API.Tests.Parser
const string rootPath = @"E:/Manga/"; const string rootPath = @"E:/Manga/";
var expected = new Dictionary<string, ParserInfo>(); var expected = new Dictionary<string, ParserInfo>();
var filepath = @"E:/Manga/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz"; var filepath = @"E:/Manga/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Mujaki no Rakuen", Volumes = "12", Series = "Mujaki no Rakuen", Volumes = "12",
Chapters = "76", Filename = "Mujaki no Rakuen Vol12 ch76.cbz", Format = MangaFormat.Archive, Chapters = "76", Filename = "Mujaki no Rakuen Vol12 ch76.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
filepath = @"E:/Manga/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz"; filepath = @"E:/Manga/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1", Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1",
@ -423,20 +423,20 @@ namespace API.Tests.Parser
} }
Assert.NotNull(actual); Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {file}"); _testOutputHelper.WriteLine($"Validating {file}");
_testOutputHelper.WriteLine("Format");
Assert.Equal(expectedInfo.Format, actual.Format); Assert.Equal(expectedInfo.Format, actual.Format);
_testOutputHelper.WriteLine("Series"); _testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expectedInfo.Series, actual.Series); Assert.Equal(expectedInfo.Series, actual.Series);
_testOutputHelper.WriteLine("Chapters"); _testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expectedInfo.Chapters, actual.Chapters); Assert.Equal(expectedInfo.Chapters, actual.Chapters);
_testOutputHelper.WriteLine("Volumes"); _testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expectedInfo.Volumes, actual.Volumes); Assert.Equal(expectedInfo.Volumes, actual.Volumes);
_testOutputHelper.WriteLine("Edition"); _testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expectedInfo.Edition, actual.Edition); Assert.Equal(expectedInfo.Edition, actual.Edition);
_testOutputHelper.WriteLine("Filename"); _testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expectedInfo.Filename, actual.Filename); Assert.Equal(expectedInfo.Filename, actual.Filename);
_testOutputHelper.WriteLine("FullFilePath"); _testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath); Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
} }
} }
} }

View File

@ -1,4 +1,6 @@
namespace API.Interfaces using System.Threading.Tasks;
namespace API.Interfaces
{ {
public interface ITaskScheduler public interface ITaskScheduler
{ {
@ -6,7 +8,7 @@
/// For use on Server startup /// For use on Server startup
/// </summary> /// </summary>
void ScheduleTasks(); void ScheduleTasks();
void ScheduleStatsTasks(); Task ScheduleStatsTasks();
void ScheduleUpdaterTasks(); void ScheduleUpdaterTasks();
void ScanLibrary(int libraryId, bool forceUpdate = false); void ScanLibrary(int libraryId, bool forceUpdate = false);
void CleanupChapters(int[] chapterIds); void CleanupChapters(int[] chapterIds);
@ -15,5 +17,6 @@
void RefreshSeriesMetadata(int libraryId, int seriesId); void RefreshSeriesMetadata(int libraryId, int seriesId);
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
void CancelStatsTasks(); void CancelStatsTasks();
void RunStatCollection();
} }
} }

View File

@ -6,8 +6,6 @@ namespace API.Interfaces.Services
public interface IStatsService public interface IStatsService
{ {
Task PathData(ClientInfoDto clientInfoDto); Task PathData(ClientInfoDto clientInfoDto);
Task FinalizeStats();
Task CollectRelevantData();
Task CollectAndSendStatsData(); Task CollectAndSendStatsData();
} }
} }

View File

@ -2,7 +2,6 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Interfaces; using API.Interfaces;
using API.Interfaces.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@ -27,7 +26,10 @@ namespace API.Services.HostedServices
try try
{ {
await ManageStartupStatsTasks(scope, taskScheduler); // These methods will automatically check if stat collection is disabled to prevent sending any data regardless
// of when setting was changed
await taskScheduler.ScheduleStatsTasks();
taskScheduler.RunStatCollection();
} }
catch (Exception) catch (Exception)
{ {
@ -35,21 +37,6 @@ namespace API.Services.HostedServices
} }
} }
private async Task ManageStartupStatsTasks(IServiceScope serviceScope, ITaskScheduler taskScheduler)
{
var unitOfWork = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>();
var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (!settingsDto.AllowStatCollection) return;
taskScheduler.ScheduleStatsTasks();
var statsService = serviceScope.ServiceProvider.GetRequiredService<IStatsService>();
await statsService.CollectAndSendStatsData();
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
} }
} }

View File

@ -22,6 +22,7 @@ namespace API.Services
private readonly IStatsService _statsService; private readonly IStatsService _statsService;
private readonly IVersionUpdaterService _versionUpdaterService; private readonly IVersionUpdaterService _versionUpdaterService;
private const string SendDataTask = "finalize-stats";
public static BackgroundJobServer Client => new BackgroundJobServer(); public static BackgroundJobServer Client => new BackgroundJobServer();
@ -76,19 +77,17 @@ namespace API.Services
#region StatsTasks #region StatsTasks
private const string SendDataTask = "finalize-stats";
public void ScheduleStatsTasks() public async Task ScheduleStatsTasks()
{ {
var allowStatCollection = bool.Parse(Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.AllowStatCollection)).GetAwaiter().GetResult().Value); var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection;
if (!allowStatCollection) if (!allowStatCollection)
{ {
_logger.LogDebug("User has opted out of stat collection, not registering tasks"); _logger.LogDebug("User has opted out of stat collection, not registering tasks");
return; return;
} }
_logger.LogDebug("Adding StatsTasks"); _logger.LogDebug("Scheduling stat collection daily");
_logger.LogDebug("Scheduling Send data to the Stats server {Setting}", nameof(Cron.Daily));
RecurringJob.AddOrUpdate(SendDataTask, () => _statsService.CollectAndSendStatsData(), Cron.Daily); RecurringJob.AddOrUpdate(SendDataTask, () => _statsService.CollectAndSendStatsData(), Cron.Daily);
} }
@ -99,6 +98,12 @@ namespace API.Services
RecurringJob.RemoveIfExists(SendDataTask); RecurringJob.RemoveIfExists(SendDataTask);
} }
public void RunStatCollection()
{
_logger.LogInformation("Enqueuing stat collection");
BackgroundJob.Enqueue(() => _statsService.CollectAndSendStatsData());
}
#endregion #endregion
#region UpdateTasks #region UpdateTasks

View File

@ -50,7 +50,7 @@ namespace API.Services.Tasks
await SaveFile(statisticsDto); await SaveFile(statisticsDto);
} }
public async Task CollectRelevantData() private async Task CollectRelevantData()
{ {
_logger.LogDebug("Collecting data from the server and database"); _logger.LogDebug("Collecting data from the server and database");
@ -63,7 +63,7 @@ namespace API.Services.Tasks
await PathData(serverInfo, usageInfo); await PathData(serverInfo, usageInfo);
} }
public async Task FinalizeStats() private async Task FinalizeStats()
{ {
try try
{ {
@ -86,6 +86,12 @@ namespace API.Services.Tasks
public async Task CollectAndSendStatsData() public async Task CollectAndSendStatsData()
{ {
var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection;
if (!allowStatCollection)
{
_logger.LogDebug("User has opted out of stat collection, not registering tasks");
return;
}
await CollectRelevantData(); await CollectRelevantData();
await FinalizeStats(); await FinalizeStats();
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Security.Cryptography;
using System.Text; using System.Text;
namespace Kavita.Common namespace Kavita.Common

View File

@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Tests", "API.Tests\API.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Common", "Kavita.Common\Kavita.Common.csproj", "{165A86F5-9E74-4C05-9305-A6F0BA32C9EE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Common", "Kavita.Common\Kavita.Common.csproj", "{165A86F5-9E74-4C05-9305-A6F0BA32C9EE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Benchmark", "API.Benchmark\API.Benchmark.csproj", "{3D781D18-2452-421F-A81A-59254FEE1FEC}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -58,5 +60,17 @@ Global
{165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x64.Build.0 = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x64.Build.0 = Release|Any CPU
{165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.ActiveCfg = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.ActiveCfg = Release|Any CPU
{165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.Build.0 = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.Build.0 = Release|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x64.Build.0 = Debug|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x86.Build.0 = Debug|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|Any CPU.Build.0 = Release|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x64.ActiveCfg = Release|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x64.Build.0 = Release|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x86.ActiveCfg = Release|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -187,13 +187,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
} }
transformKeyToOpdsUrl(key: string) { transformKeyToOpdsUrl(key: string) {
let apiUrl = environment.apiUrl; return `${location.origin}/api/opds/${key}`;
if (environment.production) {
apiUrl = `${location.protocol}//${location.origin}`;
if (location.port != '80') {
apiUrl += ':' + location.port;
}
}
return `${apiUrl}opds/${key}`;
} }
} }