Logging Enhancements (#1521)

* Recreated Kavita Logging with Serilog instead of Default. This needs to be move out of the appsettings now, to allow auto updater to patch.

* Refactored the code to be completely configured via Code rather than appsettings.json. This is a required step for Auto Updating.

* Added in the ability to send logs directly to the UI only for users on the log route. Stopping implementation as Alerts page will handle the rest of the implementation.

* Fixed up the backup service to not rely on Config from appsettings.json

* Tweaked the Logging levels available

* Moved everything over to File-scoped namespaces

* Moved everything over to File-scoped namespaces

* Code cleanup, removed an old migration and changed so debug logging doesn't print sensitive db data

* Removed dead code
This commit is contained in:
Joseph Milazzo 2022-09-12 19:25:48 -05:00 committed by GitHub
parent 9f715cc35f
commit d1a14f7e68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
212 changed files with 16599 additions and 16834 deletions

View file

@ -15,74 +15,71 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace API.Extensions
namespace API.Extensions;
public static class ApplicationServiceExtensions
{
public static class ApplicationServiceExtensions
public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
{
public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IDirectoryService, DirectoryService>();
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IFileSystem, FileSystem>();
services.AddScoped<IFileService, FileService>();
services.AddScoped<ICacheHelper, CacheHelper>();
services.AddScoped<IStatsService, StatsService>();
services.AddScoped<ITaskScheduler, TaskScheduler>();
services.AddScoped<ICacheService, CacheService>();
services.AddScoped<IArchiveService, ArchiveService>();
services.AddScoped<IBackupService, BackupService>();
services.AddScoped<ICleanupService, CleanupService>();
services.AddScoped<IBookService, BookService>();
services.AddScoped<IImageService, ImageService>();
services.AddScoped<IVersionUpdaterService, VersionUpdaterService>();
services.AddScoped<IDownloadService, DownloadService>();
services.AddScoped<IReaderService, ReaderService>();
services.AddScoped<IReadingItemService, ReadingItemService>();
services.AddScoped<IAccountService, AccountService>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IBookmarkService, BookmarkService>();
services.AddScoped<IThemeService, ThemeService>();
services.AddScoped<ISeriesService, SeriesService>();
services.AddScoped<IProcessSeries, ProcessSeries>();
services.AddScoped<IReadingListService, ReadingListService>();
services.AddScoped<IScannerService, ScannerService>();
services.AddScoped<IMetadataService, MetadataService>();
services.AddScoped<IWordCountAnalyzerService, WordCountAnalyzerService>();
services.AddScoped<ILibraryWatcher, LibraryWatcher>();
services.AddScoped<IPresenceTracker, PresenceTracker>();
services.AddScoped<IEventHub, EventHub>();
services.AddSqLite(config, env);
services.AddLogging(config);
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
}
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
IHostEnvironment env)
{
services.AddDbContext<DataContext>(options =>
{
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging(env.IsDevelopment());
});
}
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IDirectoryService, DirectoryService>();
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IFileSystem, FileSystem>();
services.AddScoped<IFileService, FileService>();
services.AddScoped<ICacheHelper, CacheHelper>();
services.AddScoped<IStatsService, StatsService>();
services.AddScoped<ITaskScheduler, TaskScheduler>();
services.AddScoped<ICacheService, CacheService>();
services.AddScoped<IArchiveService, ArchiveService>();
services.AddScoped<IBackupService, BackupService>();
services.AddScoped<ICleanupService, CleanupService>();
services.AddScoped<IBookService, BookService>();
services.AddScoped<IImageService, ImageService>();
services.AddScoped<IVersionUpdaterService, VersionUpdaterService>();
services.AddScoped<IDownloadService, DownloadService>();
services.AddScoped<IReaderService, ReaderService>();
services.AddScoped<IReadingItemService, ReadingItemService>();
services.AddScoped<IAccountService, AccountService>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IBookmarkService, BookmarkService>();
services.AddScoped<IThemeService, ThemeService>();
services.AddScoped<ISeriesService, SeriesService>();
services.AddScoped<IProcessSeries, ProcessSeries>();
services.AddScoped<IReadingListService, ReadingListService>();
services.AddScoped<IScannerService, ScannerService>();
services.AddScoped<IMetadataService, MetadataService>();
services.AddScoped<IWordCountAnalyzerService, WordCountAnalyzerService>();
services.AddScoped<ILibraryWatcher, LibraryWatcher>();
services.AddScoped<IPresenceTracker, PresenceTracker>();
services.AddScoped<IEventHub, EventHub>();
services.AddSqLite(config, env);
services.AddLogging(config);
services.AddSignalR(opt => opt.EnableDetailedErrors = true);
}
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
IHostEnvironment env)
private static void AddLogging(this IServiceCollection services, IConfiguration config)
{
services.AddLogging(loggingBuilder =>
{
services.AddDbContext<DataContext>(options =>
{
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.LogLevel.Equals("Debug"));
});
}
private static void AddLogging(this IServiceCollection services, IConfiguration config)
{
services.AddLogging(loggingBuilder =>
{
var loggingSection = config.GetSection("Logging");
loggingBuilder.AddFile(loggingSection);
});
}
});
}
}

View file

@ -3,33 +3,32 @@ using System.Linq;
using API.Entities;
using API.Parser;
namespace API.Extensions
{
public static class ChapterListExtensions
{
/// <summary>
/// Returns first chapter in the list with at least one file
/// </summary>
/// <param name="chapters"></param>
/// <returns></returns>
public static Chapter GetFirstChapterWithFiles(this IList<Chapter> chapters)
{
return chapters.FirstOrDefault(c => c.Files.Any());
}
namespace API.Extensions;
/// <summary>
/// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
/// </summary>
/// <param name="chapters"></param>
/// <param name="info"></param>
/// <returns></returns>
public static Chapter GetChapterByRange(this IList<Chapter> chapters, ParserInfo info)
{
var specialTreatment = info.IsSpecialInfo();
return specialTreatment
? chapters.FirstOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath)))
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
}
public static class ChapterListExtensions
{
/// <summary>
/// Returns first chapter in the list with at least one file
/// </summary>
/// <param name="chapters"></param>
/// <returns></returns>
public static Chapter GetFirstChapterWithFiles(this IList<Chapter> chapters)
{
return chapters.FirstOrDefault(c => c.Files.Any());
}
/// <summary>
/// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info
/// is <see cref="ParserInfo.IsSpecial"/> then, the filename is used to search against Range or if filename exists within Files of said Chapter.
/// </summary>
/// <param name="chapters"></param>
/// <param name="info"></param>
/// <returns></returns>
public static Chapter GetChapterByRange(this IList<Chapter> chapters, ParserInfo info)
{
var specialTreatment = info.IsSpecialInfo();
return specialTreatment
? chapters.FirstOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath)))
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
}
}

View file

@ -1,15 +1,14 @@
using System.Security.Claims;
using Kavita.Common;
namespace API.Extensions
namespace API.Extensions;
public static class ClaimsPrincipalExtensions
{
public static class ClaimsPrincipalExtensions
public static string GetUsername(this ClaimsPrincipal user)
{
public static string GetUsername(this ClaimsPrincipal user)
{
var userClaim = user.FindFirst(ClaimTypes.NameIdentifier);
if (userClaim == null) throw new KavitaException("User is not authenticated");
return userClaim.Value;
}
var userClaim = user.FindFirst(ClaimTypes.NameIdentifier);
if (userClaim == null) throw new KavitaException("User is not authenticated");
return userClaim.Value;
}
}
}

View file

@ -1,16 +1,15 @@
using Microsoft.Extensions.Configuration;
namespace API.Extensions
namespace API.Extensions;
public static class ConfigurationExtensions
{
public static class ConfigurationExtensions
public static int GetMaxRollingFiles(this IConfiguration config)
{
public static int GetMaxRollingFiles(this IConfiguration config)
{
return int.Parse(config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value);
}
public static string GetLoggingFileName(this IConfiguration config)
{
return config.GetSection("Logging").GetSection("File").GetSection("Path").Value;
}
return int.Parse(config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value);
}
}
public static string GetLoggingFileName(this IConfiguration config)
{
return config.GetSection("Logging").GetSection("File").GetSection("Path").Value;
}
}

View file

@ -3,29 +3,28 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace API.Extensions
namespace API.Extensions;
public static class EnumerableExtensions
{
public static class EnumerableExtensions
private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500));
/// <summary>
/// A natural sort implementation
/// </summary>
/// <param name="items">IEnumerable to process</param>
/// <param name="selector">Function that produces a string. Does not support null values</param>
/// <param name="stringComparer">Defaults to CurrentCulture</param>
/// <typeparam name="T"></typeparam>
/// <returns>Sorted Enumerable</returns>
public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
{
private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500));
var list = items.ToList();
var maxDigits = list
.SelectMany(i => Regex.Matches(selector(i))
.Select(digitChunk => (int?)digitChunk.Value.Length))
.Max() ?? 0;
/// <summary>
/// A natural sort implementation
/// </summary>
/// <param name="items">IEnumerable to process</param>
/// <param name="selector">Function that produces a string. Does not support null values</param>
/// <param name="stringComparer">Defaults to CurrentCulture</param>
/// <typeparam name="T"></typeparam>
/// <returns>Sorted Enumerable</returns>
public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
{
var list = items.ToList();
var maxDigits = list
.SelectMany(i => Regex.Matches(selector(i))
.Select(digitChunk => (int?)digitChunk.Value.Length))
.Max() ?? 0;
return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
}
return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
}
}

View file

@ -1,19 +1,18 @@
using System;
using System.IO;
namespace API.Extensions
namespace API.Extensions;
public static class FileInfoExtensions
{
public static class FileInfoExtensions
/// <summary>
/// Checks if the last write time of the file is after the passed date
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="comparison"></param>
/// <returns></returns>
public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison)
{
/// <summary>
/// Checks if the last write time of the file is after the passed date
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="comparison"></param>
/// <returns></returns>
public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison)
{
return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0;
}
return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0;
}
}

View file

@ -3,20 +3,19 @@ using System.Collections.Generic;
using API.DTOs.Filtering;
using API.Entities.Enums;
namespace API.Extensions
namespace API.Extensions;
public static class FilterDtoExtensions
{
public static class FilterDtoExtensions
private static readonly IList<MangaFormat> AllFormats = Enum.GetValues<MangaFormat>();
public static IList<MangaFormat> GetSqlFilter(this FilterDto filter)
{
private static readonly IList<MangaFormat> AllFormats = Enum.GetValues<MangaFormat>();
public static IList<MangaFormat> GetSqlFilter(this FilterDto filter)
if (filter.Formats == null || filter.Formats.Count == 0)
{
if (filter.Formats == null || filter.Formats.Count == 0)
{
return AllFormats;
}
return filter.Formats;
return AllFormats;
}
return filter.Formats;
}
}

View file

@ -9,53 +9,52 @@ using API.Helpers;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace API.Extensions
namespace API.Extensions;
public static class HttpExtensions
{
public static class HttpExtensions
public static void AddPaginationHeader(this HttpResponse response, int currentPage,
int itemsPerPage, int totalItems, int totalPages)
{
public static void AddPaginationHeader(this HttpResponse response, int currentPage,
int itemsPerPage, int totalItems, int totalPages)
var paginationHeader = new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);
var options = new JsonSerializerOptions()
{
var paginationHeader = new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);
var options = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options));
response.Headers.Add("Access-Control-Expose-Headers", "Pagination");
}
response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options));
response.Headers.Add("Access-Control-Expose-Headers", "Pagination");
}
/// <summary>
/// Calculates SHA256 hash for a byte[] and sets as ETag. Ensures Cache-Control: private header is added.
/// </summary>
/// <param name="response"></param>
/// <param name="content">If byte[] is null or empty, will only add cache-control</param>
public static void AddCacheHeader(this HttpResponse response, byte[] content)
/// <summary>
/// Calculates SHA256 hash for a byte[] and sets as ETag. Ensures Cache-Control: private header is added.
/// </summary>
/// <param name="response"></param>
/// <param name="content">If byte[] is null or empty, will only add cache-control</param>
public static void AddCacheHeader(this HttpResponse response, byte[] content)
{
if (content is not {Length: > 0}) return;
using var sha1 = SHA256.Create();
response.Headers.Add(HeaderNames.ETag, string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2"))));
response.Headers.CacheControl = $"private,max-age=100";
}
/// <summary>
/// Calculates SHA256 hash for a cover image filename and sets as ETag. Ensures Cache-Control: private header is added.
/// </summary>
/// <param name="response"></param>
/// <param name="filename"></param>
/// <param name="maxAge">Maximum amount of seconds to set for Cache-Control</param>
public static void AddCacheHeader(this HttpResponse response, string filename, int maxAge = 10)
{
if (filename is not {Length: > 0}) return;
var hashContent = filename + File.GetLastWriteTimeUtc(filename);
using var sha1 = SHA256.Create();
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
if (maxAge != 10)
{
if (content is not {Length: > 0}) return;
using var sha1 = SHA256.Create();
response.Headers.Add(HeaderNames.ETag, string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2"))));
response.Headers.CacheControl = $"private,max-age=100";
}
/// <summary>
/// Calculates SHA256 hash for a cover image filename and sets as ETag. Ensures Cache-Control: private header is added.
/// </summary>
/// <param name="response"></param>
/// <param name="filename"></param>
/// <param name="maxAge">Maximum amount of seconds to set for Cache-Control</param>
public static void AddCacheHeader(this HttpResponse response, string filename, int maxAge = 10)
{
if (filename is not {Length: > 0}) return;
var hashContent = filename + File.GetLastWriteTimeUtc(filename);
using var sha1 = SHA256.Create();
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2"))));
if (maxAge != 10)
{
response.Headers.CacheControl = $"max-age={maxAge}";
}
response.Headers.CacheControl = $"max-age={maxAge}";
}
}
}

View file

@ -10,79 +10,78 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace API.Extensions
namespace API.Extensions;
public static class IdentityServiceExtensions
{
public static class IdentityServiceExtensions
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
{
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
services.Configure<IdentityOptions>(options =>
{
services.Configure<IdentityOptions>(options =>
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+/";
});
services.AddIdentityCore<AppUser>(opt =>
{
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+/";
});
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequireDigit = false;
opt.Password.RequireDigit = false;
opt.Password.RequireLowercase = false;
opt.Password.RequireUppercase = false;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequiredLength = 6;
services.AddIdentityCore<AppUser>(opt =>
opt.SignIn.RequireConfirmedEmail = true;
opt.Lockout.AllowedForNewUsers = true;
opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
opt.Lockout.MaxFailedAccessAttempts = 5;
})
.AddTokenProvider<DataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider)
.AddRoles<AppRole>()
.AddRoleManager<RoleManager<AppRole>>()
.AddSignInManager<SignInManager<AppUser>>()
.AddRoleValidator<RoleValidator<AppRole>>()
.AddEntityFrameworkStores<DataContext>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequireDigit = false;
opt.Password.RequireDigit = false;
opt.Password.RequireLowercase = false;
opt.Password.RequireUppercase = false;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequiredLength = 6;
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])),
ValidateIssuer = false,
ValidateAudience = false,
ValidIssuer = "Kavita"
};
opt.SignIn.RequireConfirmedEmail = true;
opt.Lockout.AllowedForNewUsers = true;
opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
opt.Lockout.MaxFailedAccessAttempts = 5;
})
.AddTokenProvider<DataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider)
.AddRoles<AppRole>()
.AddRoleManager<RoleManager<AppRole>>()
.AddSignInManager<SignInManager<AppUser>>()
.AddRoleValidator<RoleValidator<AppRole>>()
.AddEntityFrameworkStores<DataContext>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
options.Events = new JwtBearerEvents()
{
options.TokenValidationParameters = new TokenValidationParameters()
OnMessageReceived = context =>
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])),
ValidateIssuer = false,
ValidateAudience = false,
ValidIssuer = "Kavita"
};
options.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
// Only use query string based token on SignalR hubs
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
// Only use query string based token on SignalR hubs
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
context.Token = accessToken;
}
};
});
services.AddAuthorization(opt =>
{
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
opt.AddPolicy("RequireChangePasswordRole", policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole));
});
return services;
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization(opt =>
{
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
opt.AddPolicy("RequireChangePasswordRole", policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole));
});
return services;
}
}

View file

@ -3,31 +3,30 @@ using System.Linq;
using API.Entities;
using API.Parser;
namespace API.Extensions
{
public static class ParserInfoListExtensions
{
/// <summary>
/// Selects distinct volume numbers by the "Volumes" key on the ParserInfo
/// </summary>
/// <param name="infos"></param>
/// <returns></returns>
public static IList<string> DistinctVolumes(this IList<ParserInfo> infos)
{
return infos.Select(p => p.Volumes).Distinct().ToList();
}
namespace API.Extensions;
/// <summary>
/// Checks if a list of ParserInfos has a given chapter or not. Lookup occurs on Range property. If a chapter is
/// special, then the <see cref="ParserInfo.Filename"/> is matched, else the <see cref="ParserInfo.Chapters"/> field is checked.
/// </summary>
/// <param name="infos"></param>
/// <param name="chapter"></param>
/// <returns></returns>
public static bool HasInfo(this IList<ParserInfo> infos, Chapter chapter)
{
return chapter.IsSpecial ? infos.Any(v => v.Filename == chapter.Range)
: infos.Any(v => v.Chapters == chapter.Range);
}
public static class ParserInfoListExtensions
{
/// <summary>
/// Selects distinct volume numbers by the "Volumes" key on the ParserInfo
/// </summary>
/// <param name="infos"></param>
/// <returns></returns>
public static IList<string> DistinctVolumes(this IList<ParserInfo> infos)
{
return infos.Select(p => p.Volumes).Distinct().ToList();
}
/// <summary>
/// Checks if a list of ParserInfos has a given chapter or not. Lookup occurs on Range property. If a chapter is
/// special, then the <see cref="ParserInfo.Filename"/> is matched, else the <see cref="ParserInfo.Chapters"/> field is checked.
/// </summary>
/// <param name="infos"></param>
/// <param name="chapter"></param>
/// <returns></returns>
public static bool HasInfo(this IList<ParserInfo> infos, Chapter chapter)
{
return chapter.IsSpecial ? infos.Any(v => v.Filename == chapter.Range)
: infos.Any(v => v.Chapters == chapter.Range);
}
}

View file

@ -4,46 +4,45 @@ using API.Entities;
using API.Parser;
using API.Services.Tasks.Scanner;
namespace API.Extensions
namespace API.Extensions;
public static class SeriesExtensions
{
public static class SeriesExtensions
/// <summary>
/// Checks against all the name variables of the Series if it matches anything in the list. This does not check against format.
/// </summary>
/// <param name="series"></param>
/// <param name="list"></param>
/// <returns></returns>
public static bool NameInList(this Series series, IEnumerable<string> list)
{
/// <summary>
/// Checks against all the name variables of the Series if it matches anything in the list. This does not check against format.
/// </summary>
/// <param name="series"></param>
/// <param name="list"></param>
/// <returns></returns>
public static bool NameInList(this Series series, IEnumerable<string> list)
{
return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|| name == series.Name || name == series.LocalizedName || name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName));
}
return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|| name == series.Name || name == series.LocalizedName || name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName));
}
/// <summary>
/// Checks against all the name variables of the Series if it matches anything in the list. Includes a check against the Format of the Series
/// </summary>
/// <param name="series"></param>
/// <param name="list"></param>
/// <returns></returns>
public static bool NameInList(this Series series, IEnumerable<ParsedSeries> list)
{
return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|| name.Name == series.Name || name.Name == series.LocalizedName || name.Name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName) && series.Format == name.Format);
}
/// <summary>
/// Checks against all the name variables of the Series if it matches anything in the list. Includes a check against the Format of the Series
/// </summary>
/// <param name="series"></param>
/// <param name="list"></param>
/// <returns></returns>
public static bool NameInList(this Series series, IEnumerable<ParsedSeries> list)
{
return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|| name.Name == series.Name || name.Name == series.LocalizedName || name.Name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName) && series.Format == name.Format);
}
/// <summary>
/// Checks against all the name variables of the Series if it matches the <see cref="ParserInfo"/>
/// </summary>
/// <param name="series"></param>
/// <param name="info"></param>
/// <returns></returns>
public static bool NameInParserInfo(this Series series, ParserInfo info)
{
if (info == null) return false;
return Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|| info.Series == series.Name || info.Series == series.LocalizedName || info.Series == series.OriginalName
|| Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName);
}
/// <summary>
/// Checks against all the name variables of the Series if it matches the <see cref="ParserInfo"/>
/// </summary>
/// <param name="series"></param>
/// <param name="info"></param>
/// <returns></returns>
public static bool NameInParserInfo(this Series series, ParserInfo info)
{
if (info == null) return false;
return Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name)
|| info.Series == series.Name || info.Series == series.LocalizedName || info.Series == series.OriginalName
|| Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName);
}
}

View file

@ -4,29 +4,28 @@ using API.Comparators;
using API.Entities;
using API.Entities.Enums;
namespace API.Extensions
{
public static class VolumeListExtensions
{
/// <summary>
/// Selects the first Volume to get the cover image from. For a book with only a special, the special will be returned.
/// If there are both specials and non-specials, then the first non-special will be returned.
/// </summary>
/// <param name="volumes"></param>
/// <param name="seriesFormat"></param>
/// <returns></returns>
public static Volume GetCoverImage(this IList<Volume> volumes, MangaFormat seriesFormat)
{
if (seriesFormat is MangaFormat.Epub or MangaFormat.Pdf)
{
return volumes.OrderBy(x => x.Number).FirstOrDefault();
}
namespace API.Extensions;
if (volumes.Any(x => x.Number != 0))
{
return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0);
}
public static class VolumeListExtensions
{
/// <summary>
/// Selects the first Volume to get the cover image from. For a book with only a special, the special will be returned.
/// If there are both specials and non-specials, then the first non-special will be returned.
/// </summary>
/// <param name="volumes"></param>
/// <param name="seriesFormat"></param>
/// <returns></returns>
public static Volume GetCoverImage(this IList<Volume> volumes, MangaFormat seriesFormat)
{
if (seriesFormat is MangaFormat.Epub or MangaFormat.Pdf)
{
return volumes.OrderBy(x => x.Number).FirstOrDefault();
}
if (volumes.Any(x => x.Number != 0))
{
return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0);
}
return volumes.OrderBy(x => x.Number).FirstOrDefault();
}
}

View file

@ -2,18 +2,17 @@
using System.IO.Compression;
using System.Linq;
namespace API.Extensions
namespace API.Extensions;
public static class ZipArchiveExtensions
{
public static class ZipArchiveExtensions
/// <summary>
/// Checks if archive has one or more files. Excludes directory entries.
/// </summary>
/// <param name="archive"></param>
/// <returns></returns>
public static bool HasFiles(this ZipArchive archive)
{
/// <summary>
/// Checks if archive has one or more files. Excludes directory entries.
/// </summary>
/// <param name="archive"></param>
/// <returns></returns>
public static bool HasFiles(this ZipArchive archive)
{
return archive.Entries.Any(x => Path.HasExtension(x.FullName));
}
return archive.Entries.Any(x => Path.HasExtension(x.FullName));
}
}
}