diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs
index 7eb7fe910..8d95d4c23 100644
--- a/API/Controllers/ServerController.cs
+++ b/API/Controllers/ServerController.cs
@@ -129,15 +129,6 @@ public class ServerController : BaseApiController
return Ok();
}
- ///
- /// Returns non-sensitive information about the current system
- ///
- ///
- [HttpGet("server-info")]
- public async Task> GetVersion()
- {
- return Ok(await _statsService.GetServerInfo());
- }
///
/// Returns non-sensitive information about the current system
@@ -145,7 +136,7 @@ public class ServerController : BaseApiController
/// This is just for the UI and is extremely lightweight
///
[HttpGet("server-info-slim")]
- public async Task> GetSlimVersion()
+ public async Task> GetSlimVersion()
{
return Ok(await _statsService.GetServerInfoSlim());
}
diff --git a/API/DTOs/Stats/FileFormatDto.cs b/API/DTOs/Stats/FileFormatDto.cs
deleted file mode 100644
index 6319bd2a9..000000000
--- a/API/DTOs/Stats/FileFormatDto.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using API.Entities.Enums;
-
-namespace API.DTOs.Stats;
-
-public class FileFormatDto
-{
- ///
- /// The extension with the ., in lowercase
- ///
- public required string Extension { get; set; }
- ///
- /// Format of extension
- ///
- public required MangaFormat Format { get; set; }
-}
diff --git a/API/DTOs/Stats/ServerInfoDto.cs b/API/DTOs/Stats/ServerInfoDto.cs
deleted file mode 100644
index 41c4c8264..000000000
--- a/API/DTOs/Stats/ServerInfoDto.cs
+++ /dev/null
@@ -1,184 +0,0 @@
-using System;
-using System.Collections.Generic;
-using API.Entities.Enums;
-
-namespace API.DTOs.Stats;
-#nullable enable
-
-///
-/// Represents information about a Kavita Installation
-///
-public class ServerInfoDto
-{
- ///
- /// Unique Id that represents a unique install
- ///
- public required string InstallId { get; set; }
- public required string Os { get; set; }
- ///
- /// If the Kavita install is using Docker
- ///
- public bool IsDocker { get; set; }
- ///
- /// Version of .NET instance is running
- ///
- public required string DotnetVersion { get; set; }
- ///
- /// Version of Kavita
- ///
- public required string KavitaVersion { get; set; }
- ///
- /// Number of Cores on the instance
- ///
- public int NumOfCores { get; set; }
- ///
- /// The number of libraries on the instance
- ///
- public int NumberOfLibraries { get; set; }
- ///
- /// Does any user have bookmarks
- ///
- public bool HasBookmarks { get; set; }
- ///
- /// The site theme the install is using
- ///
- /// Introduced in v0.5.2
- public string? ActiveSiteTheme { get; set; }
- ///
- /// The reading mode the main user has as a preference
- ///
- /// Introduced in v0.5.2
- public ReaderMode MangaReaderMode { get; set; }
-
- ///
- /// Number of users on the install
- ///
- /// Introduced in v0.5.2
- public int NumberOfUsers { get; set; }
-
- ///
- /// Number of collections on the install
- ///
- /// Introduced in v0.5.2
- public int NumberOfCollections { get; set; }
- ///
- /// Number of reading lists on the install (Sum of all users)
- ///
- /// Introduced in v0.5.2
- public int NumberOfReadingLists { get; set; }
- ///
- /// Is OPDS enabled
- ///
- /// Introduced in v0.5.2
- public bool OPDSEnabled { get; set; }
- ///
- /// Total number of files in the instance
- ///
- /// Introduced in v0.5.2
- public int TotalFiles { get; set; }
- ///
- /// Total number of Genres in the instance
- ///
- /// Introduced in v0.5.4
- public int TotalGenres { get; set; }
- ///
- /// Total number of People in the instance
- ///
- /// Introduced in v0.5.4
- public int TotalPeople { get; set; }
- ///
- /// Number of users on this instance using Card Layout
- ///
- /// Introduced in v0.5.4
- public int UsersOnCardLayout { get; set; }
- ///
- /// Number of users on this instance using List Layout
- ///
- /// Introduced in v0.5.4
- public int UsersOnListLayout { get; set; }
- ///
- /// Max number of Series for any library on the instance
- ///
- /// Introduced in v0.5.4
- public int MaxSeriesInALibrary { get; set; }
- ///
- /// Max number of Volumes for any library on the instance
- ///
- /// Introduced in v0.5.4
- public int MaxVolumesInASeries { get; set; }
- ///
- /// Max number of Chapters for any library on the instance
- ///
- /// Introduced in v0.5.4
- public int MaxChaptersInASeries { get; set; }
- ///
- /// Does this instance have relationships setup between series
- ///
- /// Introduced in v0.5.4
- public bool UsingSeriesRelationships { get; set; }
- ///
- /// A list of background colors set on the instance
- ///
- /// Introduced in v0.6.0
- public required IEnumerable MangaReaderBackgroundColors { get; set; }
- ///
- /// A list of Page Split defaults being used on the instance
- ///
- /// Introduced in v0.6.0
- public required IEnumerable MangaReaderPageSplittingModes { get; set; }
- ///
- /// A list of Layout Mode defaults being used on the instance
- ///
- /// Introduced in v0.6.0
- public required IEnumerable MangaReaderLayoutModes { get; set; }
- ///
- /// A list of file formats existing in the instance
- ///
- /// Introduced in v0.6.0
- public required IEnumerable FileFormats { get; set; }
- ///
- /// If there is at least one user that is using an age restricted profile on the instance
- ///
- /// Introduced in v0.6.0
- public bool UsingRestrictedProfiles { get; set; }
- ///
- /// Number of users using the Emulate Comic Book setting
- ///
- /// Introduced in v0.7.0
- public int UsersWithEmulateComicBook { get; set; }
- ///
- /// Percent (0.0-1.0) of libraries with folder watching enabled
- ///
- /// Introduced in v0.7.0
- public float PercentOfLibrariesWithFolderWatchingEnabled { get; set; }
- ///
- /// Percent (0.0-1.0) of libraries included in Search
- ///
- /// Introduced in v0.7.0
- public float PercentOfLibrariesIncludedInSearch { get; set; }
- ///
- /// Percent (0.0-1.0) of libraries included in Recommended
- ///
- /// Introduced in v0.7.0
- public float PercentOfLibrariesIncludedInRecommended { get; set; }
- ///
- /// Percent (0.0-1.0) of libraries included in Dashboard
- ///
- /// Introduced in v0.7.0
- public float PercentOfLibrariesIncludedInDashboard { get; set; }
- ///
- /// Total reading hours of all users
- ///
- /// Introduced in v0.7.0
- public long TotalReadingHours { get; set; }
- ///
- /// The encoding the server is using to save media
- ///
- /// Added in v0.7.3
- public EncodeFormat EncodeMediaAs { get; set; }
- ///
- /// The last user reading progress on the server (in UTC)
- ///
- /// Added in v0.7.4
- public DateTime LastReadTime { get; set; }
-}
diff --git a/API/DTOs/Stats/V3/LibraryStatV3.cs b/API/DTOs/Stats/V3/LibraryStatV3.cs
new file mode 100644
index 000000000..51af34b58
--- /dev/null
+++ b/API/DTOs/Stats/V3/LibraryStatV3.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using API.Entities.Enums;
+
+namespace API.DTOs.Stats.V3;
+
+public class LibraryStatV3
+{
+ public bool IncludeInDashboard { get; set; }
+ public bool IncludeInSearch { get; set; }
+ public bool UsingFolderWatching { get; set; }
+ ///
+ /// Are any exclude patterns setup
+ ///
+ public bool UsingExcludePatterns { get; set; }
+ ///
+ /// Will this library create collections from ComicInfo
+ ///
+ public bool CreateCollectionsFromMetadata { get; set; }
+ ///
+ /// Will this library create reading lists from ComicInfo
+ ///
+ public bool CreateReadingListsFromMetadata { get; set; }
+ ///
+ /// Type of the Library
+ ///
+ public LibraryType LibraryType { get; set; }
+ public ICollection FileTypes { get; set; }
+ ///
+ /// Last time library was fully scanned
+ ///
+ public DateTime LastScanned { get; set; }
+ ///
+ /// Number of folders the library has
+ ///
+ public int NumberOfFolders { get; set; }
+
+
+}
diff --git a/API/DTOs/Stats/V3/RelationshipStatV3.cs b/API/DTOs/Stats/V3/RelationshipStatV3.cs
new file mode 100644
index 000000000..e8e1e7440
--- /dev/null
+++ b/API/DTOs/Stats/V3/RelationshipStatV3.cs
@@ -0,0 +1,12 @@
+using API.Entities.Enums;
+
+namespace API.DTOs.Stats.V3;
+
+///
+/// KavitaStats - Information about Series Relationships
+///
+public class RelationshipStatV3
+{
+ public int Count { get; set; }
+ public RelationKind Relationship { get; set; }
+}
diff --git a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs b/API/DTOs/Stats/V3/ServerInfoV3Dto.cs
new file mode 100644
index 000000000..edc2ad2b4
--- /dev/null
+++ b/API/DTOs/Stats/V3/ServerInfoV3Dto.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using API.Entities.Enums;
+
+namespace API.DTOs.Stats.V3;
+
+///
+/// Represents information about a Kavita Installation for Kavita Stats v3 API
+///
+public class ServerInfoV3Dto
+{
+ ///
+ /// Unique Id that represents a unique install
+ ///
+ public required string InstallId { get; set; }
+ public required string Os { get; set; }
+ ///
+ /// If the Kavita install is using Docker
+ ///
+ public bool IsDocker { get; set; }
+ ///
+ /// Version of .NET instance is running
+ ///
+ public required string DotnetVersion { get; set; }
+ ///
+ /// Version of Kavita
+ ///
+ public required string KavitaVersion { get; set; }
+ ///
+ /// Version of Kavita on Installation
+ ///
+ public required string InitialKavitaVersion { get; set; }
+ ///
+ /// Date of first Installation
+ ///
+ public DateTime InitialInstallDate { get; set; }
+ ///
+ /// Number of Cores on the instance
+ ///
+ public int NumOfCores { get; set; }
+ ///
+ /// OS locale on the instance
+ ///
+ public string OsLocale { get; set; }
+ ///
+ /// Milliseconds to open a random archive (zip/cbz) for reading
+ ///
+ public long TimeToOpeCbzMs { get; set; }
+ ///
+ /// Number of pages for said archive (zip/cbz)
+ ///
+ public long TimeToOpenCbzPages { get; set; }
+ ///
+ /// Milliseconds to get a response from KavitaStats API
+ ///
+ /// This pings a health check and does not capture any IP Information
+ public long TimeToPingKavitaStatsApi { get; set; }
+
+
+
+ #region Media
+ ///
+ /// Number of collections on the install
+ ///
+ public int NumberOfCollections { get; set; }
+ ///
+ /// Number of reading lists on the install (Sum of all users)
+ ///
+ public int NumberOfReadingLists { get; set; }
+ ///
+ /// Total number of files in the instance
+ ///
+ public int TotalFiles { get; set; }
+ ///
+ /// Total number of Genres in the instance
+ ///
+ public int TotalGenres { get; set; }
+ ///
+ /// Total number of Series in the instance
+ ///
+ public int TotalSeries { get; set; }
+ ///
+ /// Total number of Libraries in the instance
+ ///
+ public int TotalLibraries { get; set; }
+ ///
+ /// Total number of People in the instance
+ ///
+ public int TotalPeople { get; set; }
+ ///
+ /// Max number of Series for any library on the instance
+ ///
+ public int MaxSeriesInALibrary { get; set; }
+ ///
+ /// Max number of Volumes for any library on the instance
+ ///
+ public int MaxVolumesInASeries { get; set; }
+ ///
+ /// Max number of Chapters for any library on the instance
+ ///
+ public int MaxChaptersInASeries { get; set; }
+ ///
+ /// Everything about the Libraries on the instance
+ ///
+ public IList Libraries { get; set; }
+ ///
+ /// Everything around Series Relationships between series
+ ///
+ public IList Relationships { get; set; }
+ #endregion
+
+ #region Server
+ ///
+ /// Is OPDS enabled
+ ///
+ public bool OpdsEnabled { get; set; }
+ ///
+ /// The encoding the server is using to save media
+ ///
+ public EncodeFormat EncodeMediaAs { get; set; }
+ ///
+ /// The last user reading progress on the server (in UTC)
+ ///
+ public DateTime LastReadTime { get; set; }
+ ///
+ /// Is this server using Kavita+
+ ///
+ public bool ActiveKavitaPlusSubscription { get; set; }
+ #endregion
+
+ #region Users
+ ///
+ /// If there is at least one user that is using an age restricted profile on the instance
+ ///
+ /// Introduced in v0.6.0
+ public bool UsingRestrictedProfiles { get; set; }
+
+ public IList Users { get; set; }
+
+ #endregion
+}
diff --git a/API/DTOs/Stats/V3/UserStatV3.cs b/API/DTOs/Stats/V3/UserStatV3.cs
new file mode 100644
index 000000000..7f4e080ba
--- /dev/null
+++ b/API/DTOs/Stats/V3/UserStatV3.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using API.Data.Misc;
+using API.Entities.Enums.Device;
+
+namespace API.DTOs.Stats.V3;
+
+public class UserStatV3
+{
+ public AgeRestriction AgeRestriction { get; set; }
+ ///
+ /// The last reading progress on the server (in UTC)
+ ///
+ public DateTime LastReadTime { get; set; }
+ ///
+ /// The last login on the server (in UTC)
+ ///
+ public DateTime LastLogin { get; set; }
+ ///
+ /// Has the user gone through email confirmation
+ ///
+ public bool IsEmailConfirmed { get; set; }
+ ///
+ /// Is the Email a valid address
+ ///
+ public bool HasValidEmail { get; set; }
+ ///
+ /// Float between 0-1 to showcase how much of the libraries a user has access to
+ ///
+ public float PercentageOfLibrariesHasAccess { get; set; }
+ ///
+ /// Number of reading lists this user created
+ ///
+ public int ReadingListsCreatedCount { get; set; }
+ ///
+ /// Number of collections this user created
+ ///
+ public int CollectionsCreatedCount { get; set; }
+ ///
+ /// Number of series in want to read for this user
+ ///
+ public int WantToReadSeriesCount { get; set; }
+ ///
+ /// Active locale for the user
+ ///
+ public string Locale { get; set; }
+ ///
+ /// Active Theme (name)
+ ///
+ public string ActiveTheme { get; set; }
+ ///
+ /// Number of series with Bookmarks created
+ ///
+ public int SeriesBookmarksCreatedCount { get; set; }
+ ///
+ /// Kavita+ only - Has an AniList Token set
+ ///
+ public bool HasAniListToken { get; set; }
+ ///
+ /// Kavita+ only - Has a MAL Token set
+ ///
+ public bool HasMALToken { get; set; }
+ ///
+ /// Number of Smart Filters a user has created
+ ///
+ public int SmartFilterCreatedCount { get; set; }
+ ///
+ /// Is the user sharing reviews
+ ///
+ public bool IsSharingReviews { get; set; }
+ ///
+ /// The number of devices setup and their platforms
+ ///
+ public ICollection DevicePlatforms { get; set; }
+ ///
+ /// Roles for this user
+ ///
+ public ICollection Roles { get; set; }
+
+
+}
diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs
index 2ac4f3936..605656d91 100644
--- a/API/Data/Repositories/LibraryRepository.cs
+++ b/API/Data/Repositories/LibraryRepository.cs
@@ -39,7 +39,7 @@ public interface ILibraryRepository
Task LibraryExists(string libraryName);
Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None);
IEnumerable GetLibraryDtosForUsernameAsync(string userName);
- Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None);
+ Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true);
Task> GetLibrariesForUserIdAsync(int userId);
IEnumerable GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None);
Task GetLibraryTypeAsync(int libraryId);
@@ -104,13 +104,16 @@ public class LibraryRepository : ILibraryRepository
///
///
///
- public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None)
+ public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true)
{
- return await _context.Library
+ var query = _context.Library
.Include(l => l.AppUsers)
.Includes(includes)
- .AsSplitQuery()
- .ToListAsync();
+ .AsSplitQuery();
+
+ if (track) return await query.ToListAsync();
+
+ return await query.AsNoTracking().ToListAsync();
}
///
diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs
index cad70c3eb..91a186746 100644
--- a/API/Data/Repositories/SeriesRepository.cs
+++ b/API/Data/Repositories/SeriesRepository.cs
@@ -164,6 +164,7 @@ public interface ISeriesRepository
Task ClearOnDeckRemoval(int seriesId, int userId);
Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None);
Task GetPlusSeriesDto(int seriesId);
+ Task GetCountAsync();
}
public class SeriesRepository : ISeriesRepository
@@ -726,6 +727,10 @@ public class SeriesRepository : ISeriesRepository
.FirstOrDefaultAsync();
}
+ public async Task GetCountAsync()
+ {
+ return await _context.Series.CountAsync();
+ }
public async Task AddSeriesModifiers(int userId, IList series)
{
diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs
index 07723bf1b..40e614e59 100644
--- a/API/Data/Repositories/UserRepository.cs
+++ b/API/Data/Repositories/UserRepository.cs
@@ -78,7 +78,7 @@ public interface IUserRepository
Task> GetAllPreferencesByThemeAsync(int themeId);
Task HasAccessToLibrary(int libraryId, int userId);
Task HasAccessToSeries(int userId, int seriesId);
- Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None);
+ Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true);
Task GetUserByConfirmationToken(string token);
Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None);
Task> GetSeriesWithRatings(int userId);
@@ -283,10 +283,17 @@ public class UserRepository : IUserRepository
.AnyAsync(s => s.Id == seriesId);
}
- public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None)
+ public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true)
{
- return await _context.AppUser
- .Includes(includeFlags)
+ var query = _context.AppUser
+ .Includes(includeFlags);
+ if (track)
+ {
+ return await query.ToListAsync();
+ }
+
+ return await query
+ .AsNoTracking()
.ToListAsync();
}
diff --git a/API/Entities/Enums/FileTypeGroup.cs b/API/Entities/Enums/FileTypeGroup.cs
index 3d33aa37c..eda039fc9 100644
--- a/API/Entities/Enums/FileTypeGroup.cs
+++ b/API/Entities/Enums/FileTypeGroup.cs
@@ -15,5 +15,4 @@ public enum FileTypeGroup
Pdf = 3,
[Description("Images")]
Images = 4
-
}
diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs
index 7a413e3f4..097c382d5 100644
--- a/API/Entities/Library.cs
+++ b/API/Entities/Library.cs
@@ -44,9 +44,6 @@ public class Library : IEntityDate, IHasCoverImage
public bool AllowScrobbling { get; set; } = true;
-
-
-
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public DateTime CreatedUtc { get; set; }
diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs
index 9cc93fefd..33ad72719 100644
--- a/API/Services/Tasks/StatsService.cs
+++ b/API/Services/Tasks/StatsService.cs
@@ -1,19 +1,24 @@
using System;
using System.Collections.Generic;
-using System.IO;
+using System.Diagnostics;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using API.Data;
+using API.Data.Misc;
using API.Data.Repositories;
using API.DTOs.Stats;
+using API.DTOs.Stats.V3;
+using API.Entities;
using API.Entities.Enums;
-using API.Entities.Enums.UserPreferences;
+using API.Services.Plus;
using Flurl.Http;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -24,7 +29,6 @@ namespace API.Services.Tasks;
public interface IStatsService
{
Task Send();
- Task GetServerInfo();
Task GetServerInfoSlim();
Task SendCancellation();
}
@@ -36,15 +40,24 @@ public class StatsService : IStatsService
private readonly ILogger _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly DataContext _context;
- private readonly IStatisticService _statisticService;
+ private readonly ILicenseService _licenseService;
+ private readonly UserManager _userManager;
+ private readonly IEmailService _emailService;
+ private readonly ICacheService _cacheService;
private const string ApiUrl = "https://stats.kavitareader.com";
+ private const string ApiKey = "MsnvA2DfQqxSK5jh"; // It's not important this is public, just a way to keep bots from hitting the API willy-nilly
- public StatsService(ILogger logger, IUnitOfWork unitOfWork, DataContext context, IStatisticService statisticService)
+ public StatsService(ILogger logger, IUnitOfWork unitOfWork, DataContext context,
+ ILicenseService licenseService, UserManager userManager, IEmailService emailService,
+ ICacheService cacheService)
{
_logger = logger;
_unitOfWork = unitOfWork;
_context = context;
- _statisticService = statisticService;
+ _licenseService = licenseService;
+ _userManager = userManager;
+ _emailService = emailService;
+ _cacheService = cacheService;
FlurlHttp.ConfigureClient(ApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
@@ -52,7 +65,7 @@ public class StatsService : IStatsService
///
/// Due to all instances firing this at the same time, we can DDOS our server. This task when fired will schedule the task to be run
- /// randomly over a 6 hour spread
+ /// randomly over a six-hour spread
///
public async Task Send()
{
@@ -71,21 +84,24 @@ public class StatsService : IStatsService
// ReSharper disable once MemberCanBePrivate.Global
public async Task SendData()
{
- var data = await GetServerInfo();
+ var sw = Stopwatch.StartNew();
+ var data = await GetStatV3Payload();
+ _logger.LogDebug("Collecting stats took {Time} ms", sw.ElapsedMilliseconds);
+ sw.Stop();
await SendDataToStatsServer(data);
}
- private async Task SendDataToStatsServer(ServerInfoDto data)
+ private async Task SendDataToStatsServer(ServerInfoV3Dto data)
{
var responseContent = string.Empty;
try
{
- var response = await (ApiUrl + "/api/v2/stats")
+ var response = await (ApiUrl + "/api/v3/stats")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
- .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
+ .WithHeader("x-api-key", ApiKey)
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(30))
@@ -112,67 +128,6 @@ public class StatsService : IStatsService
}
}
- public async Task GetServerInfo()
- {
- var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
-
- var serverInfo = new ServerInfoDto
- {
- InstallId = serverSettings.InstallId,
- Os = RuntimeInformation.OSDescription,
- KavitaVersion = serverSettings.InstallVersion,
- DotnetVersion = Environment.Version.ToString(),
- IsDocker = OsInfo.IsDocker,
- NumOfCores = Math.Max(Environment.ProcessorCount, 1),
- UsersWithEmulateComicBook = await _context.AppUserPreferences.CountAsync(p => p.EmulateBook),
- TotalReadingHours = await _statisticService.TimeSpentReadingForUsersAsync(ArraySegment.Empty, ArraySegment.Empty),
-
- PercentOfLibrariesWithFolderWatchingEnabled = await GetPercentageOfLibrariesWithFolderWatchingEnabled(),
- PercentOfLibrariesIncludedInRecommended = await GetPercentageOfLibrariesIncludedInRecommended(),
- PercentOfLibrariesIncludedInDashboard = await GetPercentageOfLibrariesIncludedInDashboard(),
- PercentOfLibrariesIncludedInSearch = await GetPercentageOfLibrariesIncludedInSearch(),
-
- HasBookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()).Any(),
- NumberOfLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(),
- NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllCollectionsAsync()).Count(),
- NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count(),
- OPDSEnabled = serverSettings.EnableOpds,
- NumberOfUsers = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Count(),
- TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles(),
- TotalGenres = await _unitOfWork.GenreRepository.GetCountAsync(),
- TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(),
- UsingSeriesRelationships = await GetIfUsingSeriesRelationship(),
- EncodeMediaAs = serverSettings.EncodeMediaAs,
- MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
- MaxVolumesInASeries = await MaxVolumesInASeries(),
- MaxChaptersInASeries = await MaxChaptersInASeries(),
- MangaReaderBackgroundColors = await AllMangaReaderBackgroundColors(),
- MangaReaderPageSplittingModes = await AllMangaReaderPageSplitting(),
- MangaReaderLayoutModes = await AllMangaReaderLayoutModes(),
- FileFormats = AllFormats(),
- UsingRestrictedProfiles = await GetUsingRestrictedProfiles(),
- LastReadTime = await _unitOfWork.AppUserProgressRepository.GetLatestProgress()
- };
-
- 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();
-
- if (firstAdminUser != null)
- {
- var firstAdminUserPref = (await _unitOfWork.UserRepository.GetPreferencesAsync(firstAdminUser.UserName!));
- var activeTheme = firstAdminUserPref?.Theme ?? Seed.DefaultThemes.First(t => t.IsDefault);
-
- serverInfo.ActiveSiteTheme = activeTheme.Name;
- if (firstAdminUserPref != null) serverInfo.MangaReaderMode = firstAdminUserPref.ReaderMode;
- }
-
- return serverInfo;
- }
public async Task GetServerInfoSlim()
{
@@ -199,7 +154,7 @@ public class StatsService : IStatsService
var response = await (ApiUrl + "/api/v2/stats/opt-out?installId=" + installId)
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
- .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
+ .WithHeader("x-api-key", ApiKey)
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(30))
@@ -220,37 +175,32 @@ public class StatsService : IStatsService
}
}
- private async Task GetPercentageOfLibrariesWithFolderWatchingEnabled()
+ private static async Task PingStatsApi()
{
- var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
- if (libraries.Count == 0) return 0.0f;
- return libraries.Count(l => l.FolderWatching) / (1.0f * libraries.Count);
- }
+ try
+ {
+ var sw = Stopwatch.StartNew();
+ var response = await (ApiUrl + "/api/health/")
+ .WithHeader("Accept", "application/json")
+ .WithHeader("User-Agent", "Kavita")
+ .WithHeader("x-api-key", ApiKey)
+ .WithHeader("x-kavita-version", BuildInfo.Version)
+ .WithHeader("Content-Type", "application/json")
+ .WithTimeout(TimeSpan.FromSeconds(30))
+ .GetAsync();
- private async Task GetPercentageOfLibrariesIncludedInRecommended()
- {
- var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
- if (libraries.Count == 0) return 0.0f;
- return libraries.Count(l => l.IncludeInRecommended) / (1.0f * libraries.Count);
- }
+ if (response.StatusCode == StatusCodes.Status200OK)
+ {
+ sw.Stop();
+ return sw.ElapsedMilliseconds;
+ }
+ }
+ catch (Exception)
+ {
+ /* Swallow */
+ }
- private async Task GetPercentageOfLibrariesIncludedInDashboard()
- {
- var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
- if (libraries.Count == 0) return 0.0f;
- return libraries.Count(l => l.IncludeInDashboard) / (1.0f * libraries.Count);
- }
-
- private async Task GetPercentageOfLibrariesIncludedInSearch()
- {
- var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
- if (libraries.Count == 0) return 0.0f;
- return libraries.Count(l => l.IncludeInSearch) / (1.0f * libraries.Count);
- }
-
- private Task GetIfUsingSeriesRelationship()
- {
- return _context.SeriesRelation.AnyAsync();
+ return 0;
}
private async Task MaxSeriesInAnyLibrary()
@@ -290,41 +240,178 @@ public class StatsService : IStatsService
.Count());
}
- private async Task> AllMangaReaderBackgroundColors()
+ private async Task GetStatV3Payload()
{
- return await _context.AppUserPreferences.Select(p => p.BackgroundColor).Distinct().ToListAsync();
- }
+ var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ var dto = new ServerInfoV3Dto()
+ {
+ InstallId = serverSettings.InstallId,
+ KavitaVersion = serverSettings.InstallVersion,
+ InitialKavitaVersion = serverSettings.FirstInstallVersion,
+ InitialInstallDate = (DateTime)serverSettings.FirstInstallDate!,
+ IsDocker = OsInfo.IsDocker,
+ Os = RuntimeInformation.OSDescription,
+ NumOfCores = Math.Max(Environment.ProcessorCount, 1),
+ DotnetVersion = Environment.Version.ToString(),
+ OpdsEnabled = serverSettings.EnableOpds,
+ EncodeMediaAs = serverSettings.EncodeMediaAs,
+ };
- private async Task> AllMangaReaderPageSplitting()
- {
- return await _context.AppUserPreferences.Select(p => p.PageSplitOption).Distinct().ToListAsync();
- }
+ dto.OsLocale = CultureInfo.CurrentCulture.EnglishName;
+ dto.LastReadTime = await _unitOfWork.AppUserProgressRepository.GetLatestProgress();
+ dto.MaxSeriesInALibrary = await MaxSeriesInAnyLibrary();
+ dto.MaxVolumesInASeries = await MaxVolumesInASeries();
+ dto.MaxChaptersInASeries = await MaxChaptersInASeries();
+ dto.TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles();
+ dto.TotalGenres = await _unitOfWork.GenreRepository.GetCountAsync();
+ dto.TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync();
+ dto.TotalSeries = await _unitOfWork.SeriesRepository.GetCountAsync();
+ dto.TotalLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count();
+ dto.NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllCollectionsAsync()).Count();
+ dto.NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count();
+
+ try
+ {
+ var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
+ dto.ActiveKavitaPlusSubscription = await _licenseService.HasActiveSubscription(license);
+ }
+ catch (Exception)
+ {
+ dto.ActiveKavitaPlusSubscription = false;
+ }
- private async Task> AllMangaReaderLayoutModes()
- {
- return await _context.AppUserPreferences.Select(p => p.LayoutMode).Distinct().ToListAsync();
- }
+ // Find a random cbz/zip file and open it for reading
+ await OpenRandomFile(dto);
+ dto.TimeToPingKavitaStatsApi = await PingStatsApi();
- private IEnumerable AllFormats()
- {
+ #region Relationships
- var results = _context.MangaFile
- .AsNoTracking()
- .AsEnumerable()
- .Select(m => new FileFormatDto()
+ dto.Relationships = await _context.SeriesRelation
+ .GroupBy(sr => sr.RelationKind)
+ .Select(g => new RelationshipStatV3
{
- Format = m.Format,
- Extension = m.Extension
+ Relationship = g.Key,
+ Count = g.Count()
})
- .DistinctBy(f => f.Extension)
- .ToList();
+ .ToListAsync();
- return results;
+ #endregion
+
+ #region Libraries
+ var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.Folders |
+ LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns | LibraryIncludes.AppUser)).ToList();
+ dto.Libraries ??= [];
+ foreach (var library in allLibraries)
+ {
+ var libDto = new LibraryStatV3();
+ libDto.IncludeInDashboard = library.IncludeInDashboard;
+ libDto.IncludeInSearch = library.IncludeInSearch;
+ libDto.LastScanned = library.LastScanned;
+ libDto.NumberOfFolders = library.Folders.Count;
+ libDto.FileTypes = library.LibraryFileTypes.Select(s => s.FileTypeGroup).Distinct().ToList();
+ libDto.UsingExcludePatterns = library.LibraryExcludePatterns.Any(p => !string.IsNullOrEmpty(p.Pattern));
+ libDto.UsingFolderWatching = library.FolderWatching;
+ libDto.CreateCollectionsFromMetadata = library.ManageCollections;
+ libDto.CreateReadingListsFromMetadata = library.ManageReadingLists;
+
+ dto.Libraries.Add(libDto);
+ }
+ #endregion
+
+ #region Users
+
+ // Create a dictionary mapping user IDs to the libraries they have access to
+ var userLibraryAccess = allLibraries
+ .SelectMany(l => l.AppUsers.Select(appUser => new { l, appUser.Id }))
+ .GroupBy(x => x.Id)
+ .ToDictionary(g => g.Key, g => g.Select(x => x.l).ToList());
+ dto.Users ??= [];
+ var allUsers = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences
+ | AppUserIncludes.ReadingLists | AppUserIncludes.Bookmarks
+ | AppUserIncludes.Collections | AppUserIncludes.Devices
+ | AppUserIncludes.Progress | AppUserIncludes.Ratings
+ | AppUserIncludes.SmartFilters | AppUserIncludes.WantToRead, false);
+ foreach (var user in allUsers)
+ {
+ var userDto = new UserStatV3();
+ userDto.HasMALToken = !string.IsNullOrEmpty(user.MalAccessToken);
+ userDto.HasAniListToken = !string.IsNullOrEmpty(user.AniListAccessToken);
+ userDto.AgeRestriction = new AgeRestriction()
+ {
+ AgeRating = user.AgeRestriction,
+ IncludeUnknowns = user.AgeRestrictionIncludeUnknowns
+ };
+
+ userDto.Locale = user.UserPreferences.Locale;
+ userDto.Roles = [.. _userManager.GetRolesAsync(user).Result];
+ userDto.LastLogin = user.LastActiveUtc;
+ userDto.HasValidEmail = user.Email != null && _emailService.IsValidEmail(user.Email);
+ userDto.IsEmailConfirmed = user.EmailConfirmed;
+ userDto.ActiveTheme = user.UserPreferences.Theme.Name;
+ userDto.CollectionsCreatedCount = user.Collections.Count;
+ userDto.ReadingListsCreatedCount = user.ReadingLists.Count;
+ userDto.LastReadTime = user.Progresses
+ .Select(p => p.LastModifiedUtc)
+ .DefaultIfEmpty()
+ .Max();
+ userDto.DevicePlatforms = user.Devices.Select(d => d.Platform).ToList();
+ userDto.SeriesBookmarksCreatedCount = user.Bookmarks.Count;
+ userDto.SmartFilterCreatedCount = user.SmartFilters.Count;
+ userDto.WantToReadSeriesCount = user.WantToRead.Count;
+
+ if (allLibraries.Count > 0 && userLibraryAccess.TryGetValue(user.Id, out var accessibleLibraries))
+ {
+ userDto.PercentageOfLibrariesHasAccess = (1f * accessibleLibraries.Count) / allLibraries.Count;
+ }
+ else
+ {
+ userDto.PercentageOfLibrariesHasAccess = 0;
+ }
+
+ dto.Users.Add(userDto);
+ }
+
+ #endregion
+
+ return dto;
}
- private Task GetUsingRestrictedProfiles()
+ private async Task OpenRandomFile(ServerInfoV3Dto dto)
{
- return _context.Users.AnyAsync(u => u.AgeRestriction > AgeRating.NotApplicable);
+ var random = new Random();
+ List extensions = [".cbz", ".zip"];
+
+ // Count the total number of files that match the criteria
+ var count = await _context.MangaFile.AsNoTracking()
+ .Where(r => r.Extension != null && extensions.Contains(r.Extension))
+ .CountAsync();
+
+ if (count == 0)
+ {
+ dto.TimeToOpeCbzMs = 0;
+ dto.TimeToOpenCbzPages = 0;
+
+ return;
+ }
+
+ // Generate a random skip value
+ var skip = random.Next(count);
+
+ // Fetch the random file
+ var randomFile = await _context.MangaFile.AsNoTracking()
+ .Where(r => r.Extension != null && extensions.Contains(r.Extension))
+ .Skip(skip)
+ .Take(1)
+ .FirstAsync();
+
+ var sw = Stopwatch.StartNew();
+
+ await _cacheService.Ensure(randomFile.ChapterId);
+ var time = sw.ElapsedMilliseconds;
+ sw.Stop();
+
+ dto.TimeToOpeCbzMs = time;
+ dto.TimeToOpenCbzPages = randomFile.Pages;
}
}