Feat/usage stats collection (#317)

* feat: implement anonymous usage data collection

Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
Leonardo Dias 2021-06-20 19:26:35 -03:00 committed by GitHub
parent b25335acbd
commit 1c9b2572ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 613 additions and 17 deletions

3
.gitignore vendored
View File

@ -453,4 +453,5 @@ cache/
/API/cache/ /API/cache/
/API/temp/ /API/temp/
_temp/ _temp/
_output/ _output/
stats/

View File

@ -90,14 +90,31 @@ namespace API.Controllers
Configuration.UpdateLogLevel(Program.GetAppSettingFilename(), updateSettingsDto.LoggingLevel); Configuration.UpdateLogLevel(Program.GetAppSettingFilename(), updateSettingsDto.LoggingLevel);
_unitOfWork.SettingsRepository.Update(setting); _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 + ""; _configuration.GetSection("Logging:LogLevel:Default").Value = updateSettingsDto.LoggingLevel + "";
if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated"); if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated");
if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync()) if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync())
{
await _unitOfWork.RollbackAsync();
return BadRequest("There was a critical issue. Please try again."); return BadRequest("There was a critical issue. Please try again.");
}
_logger.LogInformation("Server Settings updated"); _logger.LogInformation("Server Settings updated");
_taskScheduler.ScheduleTasks(); _taskScheduler.ScheduleTasks();
return Ok(updateSettingsDto); return Ok(updateSettingsDto);

View File

@ -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<StatsController> _logger;
private readonly IStatsService _statsService;
public StatsController(ILogger<StatsController> logger, IStatsService statsService)
{
_logger = logger;
_statsService = statsService;
}
[AllowAnonymous]
[HttpPost("client-info")]
public async Task<IActionResult> 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;
}
}
}
}

36
API/DTOs/ClientInfoDto.cs Normal file
View File

@ -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; }
}
}

12
API/DTOs/ServerInfoDto.cs Normal file
View File

@ -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; }
}
}

View File

@ -7,5 +7,6 @@
public string LoggingLevel { get; set; } public string LoggingLevel { get; set; }
public string TaskBackup { get; set; } public string TaskBackup { get; set; }
public int Port { get; set; } public int Port { get; set; }
public bool AllowStatCollection { get; set; }
} }
} }

24
API/DTOs/UsageInfoDto.cs Normal file
View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using API.Entities.Enums;
namespace API.DTOs
{
public class UsageInfoDto
{
public UsageInfoDto()
{
FileTypes = new HashSet<string>();
LibraryTypesCreated = new HashSet<LibInfo>();
}
public int UsersCount { get; set; }
public IEnumerable<string> FileTypes { get; set; }
public IEnumerable<LibInfo> LibraryTypesCreated { get; set; }
}
public class LibInfo
{
public LibraryType Type { get; set; }
public int Count { get; set; }
}
}

View File

@ -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<ClientInfoDto>();
}
public string InstallId { get; set; }
public DateTime LastUpdate { get; set; }
public UsageInfoDto UsageInfo { get; set; }
public ServerInfoDto ServerInfo { get; set; }
public List<ClientInfoDto> 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);
}
}
}

View File

@ -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<IEnumerable<string>> 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;
}
}
}

View File

@ -46,6 +46,7 @@ namespace API.Data
new () {Key = ServerSettingKey.TaskBackup, Value = "weekly"}, new () {Key = ServerSettingKey.TaskBackup, Value = "weekly"},
new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "backups/"))}, 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.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) foreach (var defaultSetting in defaultSettings)

View File

@ -15,6 +15,9 @@ namespace API.Entities.Enums
[Description("Port")] [Description("Port")]
Port = 4, Port = 4,
[Description("BackupDirectory")] [Description("BackupDirectory")]
BackupDirectory = 5 BackupDirectory = 5,
[Description("AllowStatCollection")]
AllowStatCollection = 6,
} }
} }

View File

@ -20,6 +20,7 @@ namespace API.Extensions
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env) public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
{ {
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly); services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
services.AddScoped<IStatsService, StatsService>();
services.AddScoped<ITaskScheduler, TaskScheduler>(); services.AddScoped<ITaskScheduler, TaskScheduler>();
services.AddScoped<IDirectoryService, DirectoryService>(); services.AddScoped<IDirectoryService, DirectoryService>();
services.AddScoped<ITokenService, TokenService>(); services.AddScoped<ITokenService, TokenService>();
@ -32,12 +33,8 @@ namespace API.Extensions
services.AddScoped<ICleanupService, CleanupService>(); services.AddScoped<ICleanupService, CleanupService>();
services.AddScoped<IBookService, BookService>(); services.AddScoped<IBookService, BookService>();
services.AddSqLite(config, env);
services.AddDbContext<DataContext>(options => services.ConfigRepositories();
{
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.GetLogLevel(Program.GetAppSettingFilename()).Equals("Debug"));
});
services.AddLogging(loggingBuilder => services.AddLogging(loggingBuilder =>
{ {
@ -47,7 +44,27 @@ namespace API.Extensions
return services; return services;
} }
private static IServiceCollection AddSqLite(this IServiceCollection services, IConfiguration config,
IWebHostEnvironment env)
{
services.AddDbContext<DataContext>(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<ISettingsRepository, SettingsRepository>();
services.AddScoped<IFileRepository, FileRepository>();
return services;
}
public static IServiceCollection AddStartupTask<T>(this IServiceCollection services) public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
where T : class, IStartupTask where T : class, IStartupTask
=> services.AddTransient<IStartupTask, T>(); => services.AddTransient<IStartupTask, T>();

View File

@ -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; using Microsoft.Extensions.DependencyInjection;
namespace API.Extensions namespace API.Extensions
@ -8,5 +11,16 @@ namespace API.Extensions
public static IServiceCollection AddStartupTask<T>(this IServiceCollection services) public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
where T : class, IStartupTask where T : class, IStartupTask
=> services.AddTransient<IStartupTask, T>(); => services.AddTransient<IStartupTask, T>();
public static IServiceCollection AddStatsClient(this IServiceCollection services, IConfiguration configuration)
{
services.AddHttpClient<StatsApiClient>(client =>
{
client.BaseAddress = new Uri("http://stats.kavitareader.com");
client.DefaultRequestHeaders.Add("api-key", "MsnvA2DfQqxSK5jh");
});
return services;
}
} }
} }

View File

@ -30,6 +30,9 @@ namespace API.Helpers.Converters
case ServerSettingKey.Port: case ServerSettingKey.Port:
destination.Port = int.Parse(row.Value); destination.Port = int.Parse(row.Value);
break; break;
case ServerSettingKey.AllowStatCollection:
destination.AllowStatCollection = bool.Parse(row.Value);
break;
} }
} }

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace API.Interfaces
{
public interface IFileRepository
{
Task<IEnumerable<string>> GetFileExtensions();
}
}

View File

@ -11,5 +11,7 @@
void RefreshMetadata(int libraryId, bool forceUpdate = true); void RefreshMetadata(int libraryId, bool forceUpdate = true);
void CleanupTemp(); void CleanupTemp();
void RefreshSeriesMetadata(int libraryId, int seriesId); void RefreshSeriesMetadata(int libraryId, int seriesId);
void ScheduleStatsTasks();
void CancelStatsTasks();
} }
} }

View File

@ -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();
}
}

View File

@ -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<StatsApiClient> _logger;
public StatsApiClient(HttpClient client, ILogger<StatsApiClient> 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;
}
}
}
}

View File

@ -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<ITaskScheduler>();
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<ISettingsRepository>();
var settingsDto = await settingsRepository.GetSettingsDtoAsync();
if (!settingsDto.AllowStatCollection) return;
taskScheduler.ScheduleStatsTasks();
var statsService = serviceScope.ServiceProvider.GetRequiredService<IStatsService>();
await statsService.CollectAndSendStatsData();
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View File

@ -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<StatsService> _logger;
private readonly IFileRepository _fileRepository;
public StatsService(StatsApiClient client, DataContext dbContext, ILogger<StatsService> 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<UsageStatisticsDto>();
_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<UsageStatisticsDto> GetData()
{
if (!FileExists) return new UsageStatisticsDto {InstallId = HashUtil.AnonymousToken()};
return await GetExistingData<UsageStatisticsDto>();
}
private async Task<UsageInfoDto> 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<T> GetExistingData<T>()
{
_logger.LogInformation("Fetching existing data from file");
var existingDataJson = await GetFileDataAsString();
_logger.LogInformation("Deserializing data from file to object");
var existingData = JsonSerializer.Deserialize<T>(existingDataJson);
return existingData;
}
private async Task<string> 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);
}
}
}

View File

@ -19,11 +19,14 @@ namespace API.Services
private readonly IBackupService _backupService; private readonly IBackupService _backupService;
private readonly ICleanupService _cleanupService; private readonly ICleanupService _cleanupService;
private readonly IStatsService _statsService;
public static BackgroundJobServer Client => new BackgroundJobServer(); public static BackgroundJobServer Client => new BackgroundJobServer();
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService, public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService) IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
ICleanupService cleanupService, IStatsService statsService)
{ {
_cacheService = cacheService; _cacheService = cacheService;
_logger = logger; _logger = logger;
@ -32,6 +35,7 @@ namespace API.Services
_metadataService = metadataService; _metadataService = metadataService;
_backupService = backupService; _backupService = backupService;
_cleanupService = cleanupService; _cleanupService = cleanupService;
_statsService = statsService;
} }
public void ScheduleTasks() public void ScheduleTasks()
@ -65,6 +69,33 @@ namespace API.Services
RecurringJob.AddOrUpdate("cleanup", () => _cleanupService.Cleanup(), Cron.Daily); 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) public void ScanLibrary(int libraryId, bool forceUpdate = false)
{ {
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId); _logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);

View File

@ -2,9 +2,9 @@ using System;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using API.Extensions; using API.Extensions;
using API.Interfaces;
using API.Middleware; using API.Middleware;
using API.Services; using API.Services;
using API.Services.HostedServices;
using Hangfire; using Hangfire;
using Hangfire.MemoryStorage; using Hangfire.MemoryStorage;
using Kavita.Common.EnvironmentInfo; using Kavita.Common.EnvironmentInfo;
@ -64,6 +64,8 @@ namespace API
services.AddResponseCaching(); services.AddResponseCaching();
services.AddStatsClient(_config);
services.AddHangfire(configuration => configuration services.AddHangfire(configuration => configuration
.UseSimpleAssemblyNameTypeSerializer() .UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings() .UseRecommendedSerializerSettings()
@ -71,11 +73,15 @@ namespace API
// Add the processing server as IHostedService // Add the processing server as IHostedService
services.AddHangfireServer(); services.AddHangfireServer();
// Add IHostedService for startup tasks
// Any services that should be bootstrapped go here
services.AddHostedService<StartupTasksHostedService>();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // 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, public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env,
IHostApplicationLifetime applicationLifetime, ITaskScheduler taskScheduler) IHostApplicationLifetime applicationLifetime)
{ {
app.UseMiddleware<ExceptionMiddleware>(); app.UseMiddleware<ExceptionMiddleware>();
@ -137,9 +143,6 @@ namespace API
{ {
Console.WriteLine($"Kavita - v{BuildInfo.Version}"); Console.WriteLine($"Kavita - v{BuildInfo.Version}");
}); });
// Any services that should be bootstrapped go here
taskScheduler.ScheduleTasks();
} }
private void OnShutdown() private void OnShutdown()

View File

@ -3,6 +3,11 @@
"DefaultConnection": "Data source=kavita.db" "DefaultConnection": "Data source=kavita.db"
}, },
"TokenKey": "super secret unguessable key", "TokenKey": "super secret unguessable key",
"StatsOptions": {
"ServerUrl": "http://localhost:5002",
"ServerSecret": "here's where the api key goes",
"SendDataAt": "23:50"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Debug",