mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
More Stat collection (#1337)
* Ensure that Scan Series triggers a file analysis task. * Tweaked concurrency for Analyze Files * Implemented new stats tracking for upcoming performance release.
This commit is contained in:
parent
77cd1bc80a
commit
c809597cf0
@ -2,49 +2,122 @@
|
|||||||
|
|
||||||
namespace API.DTOs.Stats
|
namespace API.DTOs.Stats
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information about a Kavita Installation
|
||||||
|
/// </summary>
|
||||||
public class ServerInfoDto
|
public class ServerInfoDto
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unique Id that represents a unique install
|
||||||
|
/// </summary>
|
||||||
public string InstallId { get; set; }
|
public string InstallId { get; set; }
|
||||||
public string Os { get; set; }
|
public string Os { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If the Kavita install is using Docker
|
||||||
|
/// </summary>
|
||||||
public bool IsDocker { get; set; }
|
public bool IsDocker { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Version of .NET instance is running
|
||||||
|
/// </summary>
|
||||||
public string DotnetVersion { get; set; }
|
public string DotnetVersion { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Version of Kavita
|
||||||
|
/// </summary>
|
||||||
public string KavitaVersion { get; set; }
|
public string KavitaVersion { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of Cores on the instance
|
||||||
|
/// </summary>
|
||||||
public int NumOfCores { get; set; }
|
public int NumOfCores { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The number of libraries on the instance
|
||||||
|
/// </summary>
|
||||||
public int NumberOfLibraries { get; set; }
|
public int NumberOfLibraries { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Does any user have bookmarks
|
||||||
|
/// </summary>
|
||||||
public bool HasBookmarks { get; set; }
|
public bool HasBookmarks { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The site theme the install is using
|
/// The site theme the install is using
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public string ActiveSiteTheme { get; set; }
|
public string ActiveSiteTheme { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The reading mode the main user has as a preference
|
/// The reading mode the main user has as a preference
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public ReaderMode MangaReaderMode { get; set; }
|
public ReaderMode MangaReaderMode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of users on the install
|
/// Number of users on the install
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public int NumberOfUsers { get; set; }
|
public int NumberOfUsers { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of collections on the install
|
/// Number of collections on the install
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public int NumberOfCollections { get; set; }
|
public int NumberOfCollections { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of reading lists on the install (Sum of all users)
|
/// Number of reading lists on the install (Sum of all users)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public int NumberOfReadingLists { get; set; }
|
public int NumberOfReadingLists { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is OPDS enabled
|
/// Is OPDS enabled
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public bool OPDSEnabled { get; set; }
|
public bool OPDSEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total number of files in the instance
|
/// Total number of files in the instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public int TotalFiles { get; set; }
|
public int TotalFiles { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of Genres in the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int TotalGenres { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of People in the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int TotalPeople { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Is this instance storing bookmarks as WebP
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public bool StoreBookmarksAsWebP { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of users on this instance using Card Layout
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int UsersOnCardLayout { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of users on this instance using List Layout
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int UsersOnListLayout { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of Series for any library on the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int MaxSeriesInALibrary { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of Volumes for any library on the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int MaxVolumesInASeries { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of Chapters for any library on the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int MaxChaptersInASeries { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Does this instance have relationships setup between series
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public bool UsingSeriesRelationships { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ public interface IGenreRepository
|
|||||||
Task<IList<GenreTagDto>> GetAllGenreDtosAsync();
|
Task<IList<GenreTagDto>> GetAllGenreDtosAsync();
|
||||||
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
|
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
|
||||||
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds);
|
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds);
|
||||||
|
Task<int> GetCountAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GenreRepository : IGenreRepository
|
public class GenreRepository : IGenreRepository
|
||||||
@ -72,6 +73,11 @@ public class GenreRepository : IGenreRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetCountAsync()
|
||||||
|
{
|
||||||
|
return await _context.Genre.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IList<Genre>> GetAllGenresAsync()
|
public async Task<IList<Genre>> GetAllGenresAsync()
|
||||||
{
|
{
|
||||||
return await _context.Genre.ToListAsync();
|
return await _context.Genre.ToListAsync();
|
||||||
|
@ -16,6 +16,7 @@ public interface IPersonRepository
|
|||||||
Task<IList<Person>> GetAllPeople();
|
Task<IList<Person>> GetAllPeople();
|
||||||
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
|
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
|
||||||
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds);
|
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds);
|
||||||
|
Task<int> GetCountAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PersonRepository : IPersonRepository
|
public class PersonRepository : IPersonRepository
|
||||||
@ -72,6 +73,11 @@ public class PersonRepository : IPersonRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetCountAsync()
|
||||||
|
{
|
||||||
|
return await _context.Person.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<IList<Person>> GetAllPeople()
|
public async Task<IList<Person>> GetAllPeople()
|
||||||
{
|
{
|
||||||
|
@ -57,6 +57,7 @@ public interface IUserRepository
|
|||||||
|
|
||||||
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
||||||
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
||||||
|
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserRepository : IUserRepository
|
public class UserRepository : IUserRepository
|
||||||
@ -246,6 +247,12 @@ public class UserRepository : IUserRepository
|
|||||||
.AnyAsync(library => library.AppUsers.Any(user => user.Id == userId));
|
.AnyAsync(library => library.AppUsers.Any(user => user.Id == userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags)
|
||||||
|
{
|
||||||
|
var query = AddIncludesToQuery(_context.Users.AsQueryable(), includeFlags);
|
||||||
|
return await query.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||||
{
|
{
|
||||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||||
|
@ -18,7 +18,7 @@ namespace API.Services.Tasks.Metadata;
|
|||||||
public interface IWordCountAnalyzerService
|
public interface IWordCountAnalyzerService
|
||||||
{
|
{
|
||||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
[AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
Task ScanLibrary(int libraryId, bool forceUpdate = false);
|
Task ScanLibrary(int libraryId, bool forceUpdate = false);
|
||||||
Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true);
|
Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true);
|
||||||
}
|
}
|
||||||
@ -46,7 +46,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
|
|
||||||
|
|
||||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
[AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
|
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
@ -191,6 +191,7 @@ public class ScannerService : IScannerService
|
|||||||
await CleanupDbEntities();
|
await CleanupDbEntities();
|
||||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, series.Id, false));
|
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, series.Id, false));
|
||||||
|
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(libraryId, series.Id, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
|
private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
|
||||||
|
@ -4,13 +4,15 @@ using System.Net.Http;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
|
using API.Data.Repositories;
|
||||||
using API.DTOs.Stats;
|
using API.DTOs.Stats;
|
||||||
using API.DTOs.Theme;
|
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Enums.UserPreferences;
|
||||||
using Flurl.Http;
|
using Flurl.Http;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Kavita.Common.Helpers;
|
using Kavita.Common.Helpers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services.Tasks;
|
namespace API.Services.Tasks;
|
||||||
@ -24,12 +26,14 @@ public class StatsService : IStatsService
|
|||||||
{
|
{
|
||||||
private readonly ILogger<StatsService> _logger;
|
private readonly ILogger<StatsService> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly DataContext _context;
|
||||||
private const string ApiUrl = "https://stats.kavitareader.com";
|
private const string ApiUrl = "https://stats.kavitareader.com";
|
||||||
|
|
||||||
public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork)
|
public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork, DataContext context)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_context = context;
|
||||||
|
|
||||||
FlurlHttp.ConfigureClient(ApiUrl, cli =>
|
FlurlHttp.ConfigureClient(ApiUrl, cli =>
|
||||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||||
@ -102,6 +106,8 @@ public class StatsService : IStatsService
|
|||||||
var installId = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId);
|
var installId = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId);
|
||||||
var installVersion = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
var installVersion = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||||
|
|
||||||
|
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
|
|
||||||
var serverInfo = new ServerInfoDto
|
var serverInfo = new ServerInfoDto
|
||||||
{
|
{
|
||||||
InstallId = installId.Value,
|
InstallId = installId.Value,
|
||||||
@ -114,11 +120,24 @@ public class StatsService : IStatsService
|
|||||||
NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(),
|
NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(),
|
||||||
NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(),
|
NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count(),
|
||||||
NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count(),
|
NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count(),
|
||||||
OPDSEnabled = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds,
|
OPDSEnabled = serverSettings.EnableOpds,
|
||||||
NumberOfUsers = (await _unitOfWork.UserRepository.GetAllUsers()).Count(),
|
NumberOfUsers = (await _unitOfWork.UserRepository.GetAllUsers()).Count(),
|
||||||
TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles(),
|
TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles(),
|
||||||
|
TotalGenres = await _unitOfWork.GenreRepository.GetCountAsync(),
|
||||||
|
TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(),
|
||||||
|
UsingSeriesRelationships = await GetIfUsingSeriesRelationship(),
|
||||||
|
StoreBookmarksAsWebP = serverSettings.ConvertBookmarkToWebP,
|
||||||
|
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
||||||
|
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
||||||
|
MaxChaptersInASeries = await MaxChaptersInASeries(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var usersWithPref = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences)).ToList();
|
||||||
|
serverInfo.UsersOnCardLayout =
|
||||||
|
usersWithPref.Count(u => u.UserPreferences.GlobalPageLayoutMode == PageLayoutMode.Cards);
|
||||||
|
serverInfo.UsersOnListLayout =
|
||||||
|
usersWithPref.Count(u => u.UserPreferences.GlobalPageLayoutMode == PageLayoutMode.List);
|
||||||
|
|
||||||
var firstAdminUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).FirstOrDefault();
|
var firstAdminUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).FirstOrDefault();
|
||||||
|
|
||||||
if (firstAdminUser != null)
|
if (firstAdminUser != null)
|
||||||
@ -132,4 +151,40 @@ public class StatsService : IStatsService
|
|||||||
|
|
||||||
return serverInfo;
|
return serverInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task<bool> GetIfUsingSeriesRelationship()
|
||||||
|
{
|
||||||
|
return _context.SeriesRelation.AnyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<int> MaxSeriesInAnyLibrary()
|
||||||
|
{
|
||||||
|
return _context.Series
|
||||||
|
.Select(s => new
|
||||||
|
{
|
||||||
|
LibraryId = s.LibraryId,
|
||||||
|
Count = _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series).Count()
|
||||||
|
})
|
||||||
|
.MaxAsync(d => d.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<int> MaxVolumesInASeries()
|
||||||
|
{
|
||||||
|
return _context.Volume
|
||||||
|
.Select(v => new
|
||||||
|
{
|
||||||
|
v.SeriesId,
|
||||||
|
Count = _context.Series.Where(s => s.Id == v.SeriesId).SelectMany(s => s.Volumes).Count()
|
||||||
|
})
|
||||||
|
.MaxAsync(d => d.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<int> MaxChaptersInASeries()
|
||||||
|
{
|
||||||
|
return _context.Series
|
||||||
|
.MaxAsync(s => s.Volumes
|
||||||
|
.Where(v => v.Number == 0)
|
||||||
|
.SelectMany(v => v.Chapters)
|
||||||
|
.Count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user