diff --git a/.gitignore b/.gitignore index 59b1114f5..8db0960dc 100644 --- a/.gitignore +++ b/.gitignore @@ -453,4 +453,5 @@ cache/ /API/cache/ /API/temp/ _temp/ -_output/ \ No newline at end of file +_output/ +stats/ \ No newline at end of file diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index ce68e4e5a..8677074ab 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -90,14 +90,31 @@ namespace API.Controllers Configuration.UpdateLogLevel(Program.GetAppSettingFilename(), updateSettingsDto.LoggingLevel); _unitOfWork.SettingsRepository.Update(setting); } + + if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + "" != setting.Value) + { + setting.Value = updateSettingsDto.AllowStatCollection + ""; + _unitOfWork.SettingsRepository.Update(setting); + if (!updateSettingsDto.AllowStatCollection) + { + _taskScheduler.CancelStatsTasks(); + } + else + { + _taskScheduler.ScheduleStatsTasks(); + } + } } _configuration.GetSection("Logging:LogLevel:Default").Value = updateSettingsDto.LoggingLevel + ""; if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated"); if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync()) + { + await _unitOfWork.RollbackAsync(); return BadRequest("There was a critical issue. Please try again."); - + } + _logger.LogInformation("Server Settings updated"); _taskScheduler.ScheduleTasks(); return Ok(updateSettingsDto); diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs new file mode 100644 index 000000000..f35552eec --- /dev/null +++ b/API/Controllers/StatsController.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using API.DTOs; +using API.Interfaces.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace API.Controllers +{ + public class StatsController : BaseApiController + { + private readonly ILogger _logger; + private readonly IStatsService _statsService; + + public StatsController(ILogger logger, IStatsService statsService) + { + _logger = logger; + _statsService = statsService; + } + + [AllowAnonymous] + [HttpPost("client-info")] + public async Task AddClientInfo([FromBody] ClientInfoDto clientInfoDto) + { + try + { + await _statsService.PathData(clientInfoDto); + + return Ok(); + } + catch (Exception e) + { + _logger.LogError(e, "Error updating the usage statistics"); + Console.WriteLine(e); + throw; + } + } + } +} \ No newline at end of file diff --git a/API/DTOs/ClientInfoDto.cs b/API/DTOs/ClientInfoDto.cs new file mode 100644 index 000000000..7070e64d7 --- /dev/null +++ b/API/DTOs/ClientInfoDto.cs @@ -0,0 +1,36 @@ +using System; + +namespace API.DTOs +{ + public class ClientInfoDto + { + public ClientInfoDto() + { + CollectedAt = DateTime.UtcNow; + } + + public string KavitaUiVersion { get; set; } + public string ScreenResolution { get; set; } + public string PlatformType { get; set; } + public DetailsVersion Browser { get; set; } + public DetailsVersion Os { get; set; } + + public DateTime? CollectedAt { get; set; } + + public bool IsTheSameDevice(ClientInfoDto clientInfoDto) + { + return (clientInfoDto.ScreenResolution ?? "").Equals(ScreenResolution) && + (clientInfoDto.PlatformType ?? "").Equals(PlatformType) && + (clientInfoDto.Browser?.Name ?? "").Equals(Browser?.Name) && + (clientInfoDto.Os?.Name ?? "").Equals(Os?.Name) && + clientInfoDto.CollectedAt.GetValueOrDefault().ToString("yyyy-MM-dd") + .Equals(CollectedAt.GetValueOrDefault().ToString("yyyy-MM-dd")); + } + } + + public class DetailsVersion + { + public string Name { get; set; } + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/ServerInfoDto.cs b/API/DTOs/ServerInfoDto.cs new file mode 100644 index 000000000..0f4a86d64 --- /dev/null +++ b/API/DTOs/ServerInfoDto.cs @@ -0,0 +1,12 @@ +namespace API.DTOs +{ + public class ServerInfoDto + { + public string Os { get; set; } + public string DotNetVersion { get; set; } + public string RunTimeVersion { get; set; } + public string KavitaVersion { get; set; } + public string BuildBranch { get; set; } + public string Culture { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/ServerSettingDTO.cs b/API/DTOs/ServerSettingDTO.cs index a1617ff11..9a52f9c09 100644 --- a/API/DTOs/ServerSettingDTO.cs +++ b/API/DTOs/ServerSettingDTO.cs @@ -7,5 +7,6 @@ public string LoggingLevel { get; set; } public string TaskBackup { get; set; } public int Port { get; set; } + public bool AllowStatCollection { get; set; } } } \ No newline at end of file diff --git a/API/DTOs/UsageInfoDto.cs b/API/DTOs/UsageInfoDto.cs new file mode 100644 index 000000000..ba4b06b41 --- /dev/null +++ b/API/DTOs/UsageInfoDto.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using API.Entities.Enums; + +namespace API.DTOs +{ + public class UsageInfoDto + { + public UsageInfoDto() + { + FileTypes = new HashSet(); + LibraryTypesCreated = new HashSet(); + } + + public int UsersCount { get; set; } + public IEnumerable FileTypes { get; set; } + public IEnumerable LibraryTypesCreated { get; set; } + } + + public class LibInfo + { + public LibraryType Type { get; set; } + public int Count { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/UsageStatisticsDto.cs b/API/DTOs/UsageStatisticsDto.cs new file mode 100644 index 000000000..1180401c3 --- /dev/null +++ b/API/DTOs/UsageStatisticsDto.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace API.DTOs +{ + public class UsageStatisticsDto + { + public UsageStatisticsDto() + { + MarkAsUpdatedNow(); + ClientsInfo = new List(); + } + + public string InstallId { get; set; } + public DateTime LastUpdate { get; set; } + public UsageInfoDto UsageInfo { get; set; } + public ServerInfoDto ServerInfo { get; set; } + public List ClientsInfo { get; set; } + + public void MarkAsUpdatedNow() + { + LastUpdate = DateTime.UtcNow; + } + + public void AddClientInfo(ClientInfoDto clientInfoDto) + { + if (ClientsInfo.Any(x => x.IsTheSameDevice(clientInfoDto))) return; + + ClientsInfo.Add(clientInfoDto); + } + } +} \ No newline at end of file diff --git a/API/Data/FileRepository.cs b/API/Data/FileRepository.cs new file mode 100644 index 000000000..a90ff4df5 --- /dev/null +++ b/API/Data/FileRepository.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using API.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace API.Data +{ + public class FileRepository : IFileRepository + { + private readonly DataContext _dbContext; + + public FileRepository(DataContext context) + { + _dbContext = context; + } + + public async Task> GetFileExtensions() + { + var fileExtensions = await _dbContext.MangaFile + .AsNoTracking() + .Select(x => x.FilePath) + .Distinct() + .ToArrayAsync(); + + var uniqueFileTypes = fileExtensions + .Select(Path.GetExtension) + .Where(x => x is not null) + .Distinct(); + + return uniqueFileTypes; + } + } +} \ No newline at end of file diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 01befd20c..2c7eb373b 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -46,6 +46,7 @@ namespace API.Data new () {Key = ServerSettingKey.TaskBackup, Value = "weekly"}, new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "backups/"))}, new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json + new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, }; foreach (var defaultSetting in defaultSettings) diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index 0aa5563f2..28378d4d1 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -15,6 +15,9 @@ namespace API.Entities.Enums [Description("Port")] Port = 4, [Description("BackupDirectory")] - BackupDirectory = 5 + BackupDirectory = 5, + [Description("AllowStatCollection")] + AllowStatCollection = 6, + } } \ No newline at end of file diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index b611cf4d6..c3db5c08a 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -20,6 +20,7 @@ namespace API.Extensions public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env) { services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -32,12 +33,8 @@ namespace API.Extensions services.AddScoped(); services.AddScoped(); - - services.AddDbContext(options => - { - options.UseSqlite(config.GetConnectionString("DefaultConnection")); - options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.GetLogLevel(Program.GetAppSettingFilename()).Equals("Debug")); - }); + services.AddSqLite(config, env); + services.ConfigRepositories(); services.AddLogging(loggingBuilder => { @@ -47,7 +44,27 @@ namespace API.Extensions return services; } - + + private static IServiceCollection AddSqLite(this IServiceCollection services, IConfiguration config, + IWebHostEnvironment env) + { + services.AddDbContext(options => + { + options.UseSqlite(config.GetConnectionString("DefaultConnection")); + options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.GetLogLevel(Program.GetAppSettingFilename()).Equals("Debug")); + }); + + return services; + } + + private static IServiceCollection ConfigRepositories(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + return services; + } + public static IServiceCollection AddStartupTask(this IServiceCollection services) where T : class, IStartupTask => services.AddTransient(); diff --git a/API/Extensions/ServiceCollectionExtensions.cs b/API/Extensions/ServiceCollectionExtensions.cs index d3cae4191..a9d12b471 100644 --- a/API/Extensions/ServiceCollectionExtensions.cs +++ b/API/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ -using API.Interfaces.Services; +using System; +using API.Interfaces.Services; +using API.Services.Clients; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace API.Extensions @@ -8,5 +11,16 @@ namespace API.Extensions public static IServiceCollection AddStartupTask(this IServiceCollection services) where T : class, IStartupTask => services.AddTransient(); + + public static IServiceCollection AddStatsClient(this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpClient(client => + { + client.BaseAddress = new Uri("http://stats.kavitareader.com"); + client.DefaultRequestHeaders.Add("api-key", "MsnvA2DfQqxSK5jh"); + }); + + return services; + } } } \ No newline at end of file diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index 27d1cbbae..261c1bff1 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -30,6 +30,9 @@ namespace API.Helpers.Converters case ServerSettingKey.Port: destination.Port = int.Parse(row.Value); break; + case ServerSettingKey.AllowStatCollection: + destination.AllowStatCollection = bool.Parse(row.Value); + break; } } diff --git a/API/Interfaces/IFileRepository.cs b/API/Interfaces/IFileRepository.cs new file mode 100644 index 000000000..cde587855 --- /dev/null +++ b/API/Interfaces/IFileRepository.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace API.Interfaces +{ + public interface IFileRepository + { + Task> GetFileExtensions(); + } +} \ No newline at end of file diff --git a/API/Interfaces/ITaskScheduler.cs b/API/Interfaces/ITaskScheduler.cs index 75f70c1fa..4f3aba6f8 100644 --- a/API/Interfaces/ITaskScheduler.cs +++ b/API/Interfaces/ITaskScheduler.cs @@ -11,5 +11,7 @@ void RefreshMetadata(int libraryId, bool forceUpdate = true); void CleanupTemp(); void RefreshSeriesMetadata(int libraryId, int seriesId); + void ScheduleStatsTasks(); + void CancelStatsTasks(); } } \ No newline at end of file diff --git a/API/Interfaces/Services/IStatsService.cs b/API/Interfaces/Services/IStatsService.cs new file mode 100644 index 000000000..f91a4e522 --- /dev/null +++ b/API/Interfaces/Services/IStatsService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using API.DTOs; + +namespace API.Interfaces.Services +{ + public interface IStatsService + { + Task PathData(ClientInfoDto clientInfoDto); + Task FinalizeStats(); + Task CollectRelevantData(); + Task CollectAndSendStatsData(); + } +} \ No newline at end of file diff --git a/API/Services/Clients/StatsApiClient.cs b/API/Services/Clients/StatsApiClient.cs new file mode 100644 index 000000000..10b7ba543 --- /dev/null +++ b/API/Services/Clients/StatsApiClient.cs @@ -0,0 +1,55 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using API.DTOs; +using Microsoft.Extensions.Logging; + +namespace API.Services.Clients +{ + public class StatsApiClient + { + private readonly HttpClient _client; + private readonly ILogger _logger; + + public StatsApiClient(HttpClient client, ILogger logger) + { + _client = client; + _logger = logger; + } + + public async Task SendDataToStatsServer(UsageStatisticsDto data) + { + var responseContent = string.Empty; + + try + { + var response = await _client.PostAsJsonAsync("/api/InstallationStats", data); + + responseContent = await response.Content.ReadAsStringAsync(); + + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException e) + { + var info = new + { + dataSent = data, + response = responseContent + }; + + _logger.LogError(e, "The StatsServer did not respond successfully. {Content}", info); + + Console.WriteLine(e); + throw; + } + catch (Exception e) + { + _logger.LogError(e, "An error happened during the request to the Stats Server"); + + Console.WriteLine(e); + throw; + } + } + } +} \ No newline at end of file diff --git a/API/Services/HostedServices/StartupTasksHostedService.cs b/API/Services/HostedServices/StartupTasksHostedService.cs new file mode 100644 index 000000000..dcdb22cca --- /dev/null +++ b/API/Services/HostedServices/StartupTasksHostedService.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using API.Interfaces; +using API.Interfaces.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace API.Services.HostedServices +{ + public class StartupTasksHostedService : IHostedService + { + private readonly IServiceProvider _provider; + + public StartupTasksHostedService(IServiceProvider serviceProvider) + { + _provider = serviceProvider; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using var scope = _provider.CreateScope(); + + var taskScheduler = scope.ServiceProvider.GetRequiredService(); + taskScheduler.ScheduleTasks(); + + try + { + await ManageStartupStatsTasks(scope, taskScheduler); + } + catch (Exception e) + { + //If stats startup fail the user can keep using the app + } + } + + private async Task ManageStartupStatsTasks(IServiceScope serviceScope, ITaskScheduler taskScheduler) + { + var settingsRepository = serviceScope.ServiceProvider.GetRequiredService(); + + var settingsDto = await settingsRepository.GetSettingsDtoAsync(); + + if (!settingsDto.AllowStatCollection) return; + + taskScheduler.ScheduleStatsTasks(); + + var statsService = serviceScope.ServiceProvider.GetRequiredService(); + + await statsService.CollectAndSendStatsData(); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} \ No newline at end of file diff --git a/API/Services/StatsService.cs b/API/Services/StatsService.cs new file mode 100644 index 000000000..4d5e3a315 --- /dev/null +++ b/API/Services/StatsService.cs @@ -0,0 +1,186 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using API.Data; +using API.DTOs; +using API.Interfaces; +using API.Interfaces.Services; +using API.Services.Clients; +using Kavita.Common; +using Kavita.Common.EnvironmentInfo; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Services +{ + public class StatsService : IStatsService + { + private const string TempFilePath = "stats/"; + private const string TempFileName = "app_stats.json"; + + private readonly StatsApiClient _client; + private readonly DataContext _dbContext; + private readonly ILogger _logger; + private readonly IFileRepository _fileRepository; + + public StatsService(StatsApiClient client, DataContext dbContext, ILogger logger, + IFileRepository fileRepository) + { + _client = client; + _dbContext = dbContext; + _logger = logger; + _fileRepository = fileRepository; + } + + private static string FinalPath => Path.Combine(Directory.GetCurrentDirectory(), TempFilePath, TempFileName); + private static bool FileExists => File.Exists(FinalPath); + + public async Task PathData(ClientInfoDto clientInfoDto) + { + _logger.LogInformation("Pathing client data to the file"); + + var statisticsDto = await GetData(); + + statisticsDto.AddClientInfo(clientInfoDto); + + await SaveFile(statisticsDto); + } + + public async Task CollectRelevantData() + { + _logger.LogInformation("Collecting data from the server and database"); + + _logger.LogInformation("Collecting usage info"); + var usageInfo = await GetUsageInfo(); + + _logger.LogInformation("Collecting server info"); + var serverInfo = GetServerInfo(); + + await PathData(serverInfo, usageInfo); + } + + public async Task FinalizeStats() + { + try + { + _logger.LogInformation("Finalizing Stats collection flow"); + + var data = await GetExistingData(); + + _logger.LogInformation("Sending data to the Stats server"); + await _client.SendDataToStatsServer(data); + + _logger.LogInformation("Deleting the file from disk"); + if (FileExists) File.Delete(FinalPath); + } + catch (Exception e) + { + _logger.LogError("Error Finalizing Stats collection flow", e); + throw; + } + } + + public async Task CollectAndSendStatsData() + { + await CollectRelevantData(); + await FinalizeStats(); + } + + private async Task PathData(ServerInfoDto serverInfoDto, UsageInfoDto usageInfoDto) + { + _logger.LogInformation("Pathing server and usage info to the file"); + + var data = await GetData(); + + data.ServerInfo = serverInfoDto; + data.UsageInfo = usageInfoDto; + + data.MarkAsUpdatedNow(); + + await SaveFile(data); + } + + private async ValueTask GetData() + { + if (!FileExists) return new UsageStatisticsDto {InstallId = HashUtil.AnonymousToken()}; + + return await GetExistingData(); + } + + private async Task GetUsageInfo() + { + var usersCount = await _dbContext.Users.CountAsync(); + + var libsCountByType = await _dbContext.Library + .AsNoTracking() + .GroupBy(x => x.Type) + .Select(x => new LibInfo {Type = x.Key, Count = x.Count()}) + .ToArrayAsync(); + + var uniqueFileTypes = await _fileRepository.GetFileExtensions(); + + var usageInfo = new UsageInfoDto + { + UsersCount = usersCount, + LibraryTypesCreated = libsCountByType, + FileTypes = uniqueFileTypes + }; + + return usageInfo; + } + + private static ServerInfoDto GetServerInfo() + { + var serverInfo = new ServerInfoDto + { + Os = RuntimeInformation.OSDescription, + DotNetVersion = Environment.Version.ToString(), + RunTimeVersion = RuntimeInformation.FrameworkDescription, + KavitaVersion = BuildInfo.Version.ToString(), + Culture = Thread.CurrentThread.CurrentCulture.Name, + BuildBranch = BuildInfo.Branch + }; + + return serverInfo; + } + + private async Task GetExistingData() + { + _logger.LogInformation("Fetching existing data from file"); + var existingDataJson = await GetFileDataAsString(); + + _logger.LogInformation("Deserializing data from file to object"); + var existingData = JsonSerializer.Deserialize(existingDataJson); + + return existingData; + } + + private async Task GetFileDataAsString() + { + _logger.LogInformation("Reading file from disk"); + return await File.ReadAllTextAsync(FinalPath); + } + + private async Task SaveFile(UsageStatisticsDto statisticsDto) + { + _logger.LogInformation("Saving file"); + + var finalDirectory = FinalPath.Replace(TempFileName, string.Empty); + if (!Directory.Exists(finalDirectory)) + { + _logger.LogInformation("Creating tmp directory"); + Directory.CreateDirectory(finalDirectory); + } + + _logger.LogInformation("Serializing data to write"); + var dataJson = JsonSerializer.Serialize(statisticsDto); + + _logger.LogInformation("Writing file to the disk"); + await File.WriteAllTextAsync(FinalPath, dataJson); + } + } +} \ No newline at end of file diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index b284fd9f7..61ee114b3 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -19,11 +19,14 @@ namespace API.Services private readonly IBackupService _backupService; private readonly ICleanupService _cleanupService; + private readonly IStatsService _statsService; + public static BackgroundJobServer Client => new BackgroundJobServer(); public TaskScheduler(ICacheService cacheService, ILogger logger, IScannerService scannerService, - IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService) + IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, + ICleanupService cleanupService, IStatsService statsService) { _cacheService = cacheService; _logger = logger; @@ -32,6 +35,7 @@ namespace API.Services _metadataService = metadataService; _backupService = backupService; _cleanupService = cleanupService; + _statsService = statsService; } public void ScheduleTasks() @@ -65,6 +69,33 @@ namespace API.Services RecurringJob.AddOrUpdate("cleanup", () => _cleanupService.Cleanup(), Cron.Daily); } + #region StatsTasks + + private const string SendDataTask = "finalize-stats"; + public void ScheduleStatsTasks() + { + var allowStatCollection = bool.Parse(Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.AllowStatCollection)).GetAwaiter().GetResult().Value); + if (!allowStatCollection) + { + _logger.LogDebug("User has opted out of stat collection, not registering tasks"); + return; + } + + _logger.LogDebug("Adding StatsTasks"); + + _logger.LogDebug("Scheduling Send data to the Stats server {Setting}", nameof(Cron.Daily)); + RecurringJob.AddOrUpdate(SendDataTask, () => _statsService.CollectAndSendStatsData(), Cron.Daily); + } + + public void CancelStatsTasks() + { + _logger.LogDebug("Cancelling/Removing StatsTasks"); + + RecurringJob.RemoveIfExists(SendDataTask); + } + + #endregion + public void ScanLibrary(int libraryId, bool forceUpdate = false) { _logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId); diff --git a/API/Startup.cs b/API/Startup.cs index 97d64145e..f2b648d24 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -2,9 +2,9 @@ using System; using System.IO.Compression; using System.Linq; using API.Extensions; -using API.Interfaces; using API.Middleware; using API.Services; +using API.Services.HostedServices; using Hangfire; using Hangfire.MemoryStorage; using Kavita.Common.EnvironmentInfo; @@ -64,6 +64,8 @@ namespace API services.AddResponseCaching(); + services.AddStatsClient(_config); + services.AddHangfire(configuration => configuration .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() @@ -71,11 +73,15 @@ namespace API // Add the processing server as IHostedService services.AddHangfireServer(); + + // Add IHostedService for startup tasks + // Any services that should be bootstrapped go here + services.AddHostedService(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env, - IHostApplicationLifetime applicationLifetime, ITaskScheduler taskScheduler) + IHostApplicationLifetime applicationLifetime) { app.UseMiddleware(); @@ -137,9 +143,6 @@ namespace API { Console.WriteLine($"Kavita - v{BuildInfo.Version}"); }); - - // Any services that should be bootstrapped go here - taskScheduler.ScheduleTasks(); } private void OnShutdown() diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index 119a1eb46..35e9218b9 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -3,6 +3,11 @@ "DefaultConnection": "Data source=kavita.db" }, "TokenKey": "super secret unguessable key", + "StatsOptions": { + "ServerUrl": "http://localhost:5002", + "ServerSecret": "here's where the api key goes", + "SendDataAt": "23:50" + }, "Logging": { "LogLevel": { "Default": "Debug",