Polish 2 (#3555)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Fesaa <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
parent
b858729c9e
commit
9565fe7360
57 changed files with 777 additions and 314 deletions
|
|
@ -39,6 +39,10 @@ public interface IDirectoryService
|
|||
/// </summary>
|
||||
string BookmarkDirectory { get; }
|
||||
/// <summary>
|
||||
/// Used for random files needed, like images to check against, list of countries, etc
|
||||
/// </summary>
|
||||
string AssetsDirectory { get; }
|
||||
/// <summary>
|
||||
/// Lists out top-level folders for a given directory. Filters out System and Hidden folders.
|
||||
/// </summary>
|
||||
/// <param name="rootPath">Absolute path of directory to scan.</param>
|
||||
|
|
@ -87,6 +91,7 @@ public class DirectoryService : IDirectoryService
|
|||
public string TempDirectory { get; }
|
||||
public string ConfigDirectory { get; }
|
||||
public string BookmarkDirectory { get; }
|
||||
public string AssetsDirectory { get; }
|
||||
public string SiteThemeDirectory { get; }
|
||||
public string FaviconDirectory { get; }
|
||||
public string LocalizationDirectory { get; }
|
||||
|
|
@ -120,6 +125,8 @@ public class DirectoryService : IDirectoryService
|
|||
ExistOrCreate(TempDirectory);
|
||||
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
||||
ExistOrCreate(BookmarkDirectory);
|
||||
AssetsDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "Assets");
|
||||
ExistOrCreate(AssetsDirectory);
|
||||
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
||||
ExistOrCreate(SiteThemeDirectory);
|
||||
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
|
@ -11,9 +10,11 @@ using API.Entities.Interfaces;
|
|||
using API.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Quantization;
|
||||
using Color = System.Drawing.Color;
|
||||
using Image = NetVips.Image;
|
||||
|
||||
namespace API.Services;
|
||||
|
|
@ -748,6 +749,7 @@ public class ImageService : IImageService
|
|||
entity.SecondaryColor = colors.Secondary;
|
||||
}
|
||||
|
||||
|
||||
public static Color HexToRgb(string? hex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hex)) throw new ArgumentException("Hex cannot be null");
|
||||
|
|
|
|||
|
|
@ -752,7 +752,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
await DownloadAndSetCovers(upstreamArtists);
|
||||
await DownloadAndSetPersonCovers(upstreamArtists);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -809,7 +809,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
await DownloadAndSetCovers(upstreamWriters);
|
||||
await DownloadAndSetPersonCovers(upstreamWriters);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1058,7 +1058,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
{
|
||||
try
|
||||
{
|
||||
await _coverDbService.SetSeriesCoverByUrl(series, coverUrl, false);
|
||||
await _coverDbService.SetSeriesCoverByUrl(series, coverUrl, false, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -1066,7 +1066,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
}
|
||||
}
|
||||
|
||||
private async Task DownloadAndSetCovers(List<SeriesStaffDto> people)
|
||||
private async Task DownloadAndSetPersonCovers(List<SeriesStaffDto> people)
|
||||
{
|
||||
foreach (var staff in people)
|
||||
{
|
||||
|
|
@ -1075,7 +1075,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId.Value);
|
||||
if (person != null && !string.IsNullOrEmpty(staff.ImageUrl) && string.IsNullOrEmpty(person.CoverImage))
|
||||
{
|
||||
await _coverDbService.SetPersonCoverByUrl(person, staff.ImageUrl, false);
|
||||
await _coverDbService.SetPersonCoverByUrl(person, staff.ImageUrl, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1326,11 +1326,15 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
}
|
||||
try
|
||||
{
|
||||
return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids")
|
||||
var ret = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids")
|
||||
.WithKavitaPlusHeaders(license)
|
||||
.PostJsonAsync(payload)
|
||||
.ReceiveJson<ExternalSeriesDetailDto>();
|
||||
|
||||
ret.Summary = StringHelper.SquashBreaklines(ret.Summary);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ public interface ICleanupService
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task CleanupWantToRead();
|
||||
|
||||
Task ConsolidateProgress();
|
||||
}
|
||||
/// <summary>
|
||||
/// Cleans up after operations on reoccurring basis
|
||||
|
|
@ -74,13 +76,21 @@ public class CleanupService : ICleanupService
|
|||
|
||||
_logger.LogInformation("Starting Cleanup");
|
||||
await SendProgress(0F, "Starting cleanup");
|
||||
|
||||
_logger.LogInformation("Cleaning temp directory");
|
||||
_directoryService.ClearDirectory(_directoryService.TempDirectory);
|
||||
|
||||
await SendProgress(0.1F, "Cleaning temp directory");
|
||||
CleanupCacheAndTempDirectories();
|
||||
|
||||
await SendProgress(0.25F, "Cleaning old database backups");
|
||||
_logger.LogInformation("Cleaning old database backups");
|
||||
await CleanupBackups();
|
||||
|
||||
await SendProgress(0.35F, "Consolidating Progress Events");
|
||||
_logger.LogInformation("Consolidating Progress Events");
|
||||
await ConsolidateProgress();
|
||||
|
||||
await SendProgress(0.50F, "Cleaning deleted cover images");
|
||||
_logger.LogInformation("Cleaning deleted cover images");
|
||||
await DeleteSeriesCoverImages();
|
||||
|
|
@ -226,6 +236,61 @@ public class CleanupService : ICleanupService
|
|||
_logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find any progress events that have duplicate, find the highest page read event, then copy over information from that and delete others, to leave one.
|
||||
/// </summary>
|
||||
public async Task ConsolidateProgress()
|
||||
{
|
||||
// AppUserProgress
|
||||
var allProgress = await _unitOfWork.AppUserProgressRepository.GetAllProgress();
|
||||
|
||||
// Group by the unique identifiers that would make a progress entry unique
|
||||
var duplicateGroups = allProgress
|
||||
.GroupBy(p => new
|
||||
{
|
||||
p.AppUserId,
|
||||
p.ChapterId,
|
||||
})
|
||||
.Where(g => g.Count() > 1);
|
||||
|
||||
foreach (var group in duplicateGroups)
|
||||
{
|
||||
// Find the entry with the highest pages read
|
||||
var highestProgress = group
|
||||
.OrderByDescending(p => p.PagesRead)
|
||||
.ThenByDescending(p => p.LastModifiedUtc)
|
||||
.First();
|
||||
|
||||
// Get the duplicate entries to remove (all except the highest progress)
|
||||
var duplicatesToRemove = group
|
||||
.Where(p => p.Id != highestProgress.Id)
|
||||
.ToList();
|
||||
|
||||
// Copy over any non-null BookScrollId if the highest progress entry doesn't have one
|
||||
if (string.IsNullOrEmpty(highestProgress.BookScrollId))
|
||||
{
|
||||
var firstValidScrollId = duplicatesToRemove
|
||||
.FirstOrDefault(p => !string.IsNullOrEmpty(p.BookScrollId))
|
||||
?.BookScrollId;
|
||||
|
||||
if (firstValidScrollId != null)
|
||||
{
|
||||
highestProgress.BookScrollId = firstValidScrollId;
|
||||
highestProgress.MarkModified();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the duplicates
|
||||
foreach (var duplicate in duplicatesToRemove)
|
||||
{
|
||||
_unitOfWork.AppUserProgressRepository.Remove(duplicate);
|
||||
}
|
||||
}
|
||||
|
||||
// Save changes
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task CleanupLogs()
|
||||
{
|
||||
_logger.LogInformation("Performing cleanup of logs directory");
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ using Microsoft.Extensions.Hosting;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
|
||||
|
||||
namespace API.Services.Tasks.Metadata;
|
||||
#nullable enable
|
||||
|
||||
|
|
@ -28,8 +29,8 @@ public interface ICoverDbService
|
|||
Task<string> DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat);
|
||||
Task<string?> DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat);
|
||||
Task<string?> DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url);
|
||||
Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true);
|
||||
Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true);
|
||||
Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false);
|
||||
Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -461,13 +462,39 @@ public class CoverDbService : ICoverDbService
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="person"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="fromBase64"></param>
|
||||
/// <param name="checkNoImagePlaceholder">Will check against all known null image placeholders to avoid writing it</param>
|
||||
public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false)
|
||||
{
|
||||
// TODO: Refactor checkNoImagePlaceholder bool to an action that evaluates how to process Image
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
var filePath = await CreateThumbnail(url, $"{ImageService.GetPersonFormat(person.Id)}", fromBase64);
|
||||
|
||||
// Additional check to see if downloaded image is similar and we have a higher resolution
|
||||
if (checkNoImagePlaceholder)
|
||||
{
|
||||
var matchRating = Path.Join(_directoryService.AssetsDirectory, "anilist-no-image-placeholder.jpg").GetSimilarity(Path.Join(_directoryService.CoverImageDirectory, filePath))!;
|
||||
|
||||
if (matchRating >= 0.9f)
|
||||
{
|
||||
if (string.IsNullOrEmpty(person.CoverImage))
|
||||
{
|
||||
filePath = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
filePath = Path.GetFileName(Path.Join(_directoryService.CoverImageDirectory, person.CoverImage));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
person.CoverImage = filePath;
|
||||
|
|
@ -498,7 +525,8 @@ public class CoverDbService : ICoverDbService
|
|||
/// <param name="series"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="fromBase64"></param>
|
||||
public async Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true)
|
||||
/// <param name="chooseBetterImage">If images are similar, will choose the higher quality image</param>
|
||||
public async Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
|
|
@ -506,6 +534,13 @@ public class CoverDbService : ICoverDbService
|
|||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
// Additional check to see if downloaded image is similar and we have a higher resolution
|
||||
if (chooseBetterImage)
|
||||
{
|
||||
var betterImage = Path.Join(_directoryService.CoverImageDirectory, series.CoverImage).GetBetterImage(Path.Join(_directoryService.CoverImageDirectory, filePath))!;
|
||||
filePath = Path.GetFileName(betterImage);
|
||||
}
|
||||
|
||||
series.CoverImage = filePath;
|
||||
series.CoverImageLocked = true;
|
||||
_imageService.UpdateColorScape(series);
|
||||
|
|
@ -540,6 +575,6 @@ public class CoverDbService : ICoverDbService
|
|||
filename, encodeFormat, coverImageSize.GetDimensions().Width);
|
||||
}
|
||||
|
||||
return await DownloadImageFromUrl(filename, encodeFormat, url);
|
||||
return await DownloadImageFromUrl(filename, encodeFormat, url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -515,7 +515,7 @@ public class ScannerService : IScannerService
|
|||
var shouldUseLibraryScan = !(await _unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths));
|
||||
if (!shouldUseLibraryScan)
|
||||
{
|
||||
_logger.LogError("[ScannerService] Library {LibraryName} consists of one or more Series folders, using series scan", library.Name);
|
||||
_logger.LogError("[ScannerService] Library {LibraryName} consists of one or more Series folders as a library root, using series scan", library.Name);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
|
||||
[GeneratedRegex(@"^\n*(.*?)\n+#{1,2}\s", RegexOptions.Singleline)]
|
||||
private static partial Regex BlogPartRegex();
|
||||
private static string _cacheFilePath;
|
||||
private readonly string _cacheFilePath;
|
||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromHours(1);
|
||||
|
||||
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IEventHub eventHub, IDirectoryService directoryService)
|
||||
|
|
@ -131,6 +131,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
Theme = sections.TryGetValue("Theme", out var theme) ? theme : [],
|
||||
Developer = sections.TryGetValue("Developer", out var developer) ? developer : [],
|
||||
Api = sections.TryGetValue("Api", out var api) ? api : [],
|
||||
FeatureRequests = sections.TryGetValue("Feature Requests", out var frs) ? frs : [],
|
||||
BlogPart = _markdown.Transform(blogPart.Trim()),
|
||||
UpdateBody = _markdown.Transform(prInfo.Body.Trim())
|
||||
};
|
||||
|
|
@ -305,7 +306,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
return updateDtos;
|
||||
}
|
||||
|
||||
private static async Task<IList<UpdateNotificationDto>?> TryGetCachedReleases()
|
||||
private async Task<IList<UpdateNotificationDto>?> TryGetCachedReleases()
|
||||
{
|
||||
if (!File.Exists(_cacheFilePath)) return null;
|
||||
|
||||
|
|
@ -376,6 +377,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
Theme = parsedSections.TryGetValue("Theme", out var theme) ? theme : [],
|
||||
Developer = parsedSections.TryGetValue("Developer", out var developer) ? developer : [],
|
||||
Api = parsedSections.TryGetValue("Api", out var api) ? api : [],
|
||||
FeatureRequests = parsedSections.TryGetValue("Feature Requests", out var frs) ? frs : [],
|
||||
BlogPart = blogPart
|
||||
};
|
||||
}
|
||||
|
|
@ -492,7 +494,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
return item;
|
||||
}
|
||||
|
||||
sealed class PullRequestInfo
|
||||
private sealed class PullRequestInfo
|
||||
{
|
||||
public required string Title { get; init; }
|
||||
public required string Body { get; init; }
|
||||
|
|
@ -501,25 +503,25 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
public required int Number { get; init; }
|
||||
}
|
||||
|
||||
sealed class CommitInfo
|
||||
private sealed class CommitInfo
|
||||
{
|
||||
public required string Sha { get; init; }
|
||||
public required CommitDetail Commit { get; init; }
|
||||
public required string Html_Url { get; init; }
|
||||
}
|
||||
|
||||
sealed class CommitDetail
|
||||
private sealed class CommitDetail
|
||||
{
|
||||
public required string Message { get; init; }
|
||||
public required CommitAuthor Author { get; init; }
|
||||
}
|
||||
|
||||
sealed class CommitAuthor
|
||||
private sealed class CommitAuthor
|
||||
{
|
||||
public required string Date { get; init; }
|
||||
}
|
||||
|
||||
sealed class NightlyInfo
|
||||
private sealed class NightlyInfo
|
||||
{
|
||||
public required string Version { get; init; }
|
||||
public required int PrNumber { get; init; }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.IdentityModel.Tokens.Jwt;
|
|||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Account;
|
||||
|
|
@ -36,6 +37,7 @@ public class TokenService : ITokenService
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly SymmetricSecurityKey _key;
|
||||
private const string RefreshTokenName = "RefreshToken";
|
||||
private static readonly SemaphoreSlim _refreshTokenLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
public TokenService(IConfiguration config, UserManager<AppUser> userManager, ILogger<TokenService> logger, IUnitOfWork unitOfWork)
|
||||
{
|
||||
|
|
@ -81,6 +83,8 @@ public class TokenService : ITokenService
|
|||
|
||||
public async Task<TokenRequestDto?> ValidateRefreshToken(TokenRequestDto request)
|
||||
{
|
||||
await _refreshTokenLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
|
|
@ -91,6 +95,7 @@ public class TokenService : ITokenService
|
|||
_logger.LogDebug("[RefreshToken] failed to validate due to not finding user in RefreshToken");
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByNameAsync(username);
|
||||
if (user == null)
|
||||
{
|
||||
|
|
@ -98,13 +103,19 @@ public class TokenService : ITokenService
|
|||
return null;
|
||||
}
|
||||
|
||||
var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken);
|
||||
var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider,
|
||||
RefreshTokenName, request.RefreshToken);
|
||||
if (!validated && tokenContent.ValidTo <= DateTime.UtcNow.Add(TimeSpan.FromHours(1)))
|
||||
{
|
||||
_logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove the old refresh token first
|
||||
await _userManager.RemoveAuthenticationTokenAsync(user,
|
||||
TokenOptions.DefaultProvider,
|
||||
RefreshTokenName);
|
||||
|
||||
try
|
||||
{
|
||||
user.UpdateLastActive();
|
||||
|
|
@ -121,7 +132,8 @@ public class TokenService : ITokenService
|
|||
Token = await CreateToken(user),
|
||||
RefreshToken = await CreateRefreshToken(user)
|
||||
};
|
||||
} catch (SecurityTokenExpiredException ex)
|
||||
}
|
||||
catch (SecurityTokenExpiredException ex)
|
||||
{
|
||||
// Handle expired token
|
||||
_logger.LogError(ex, "Failed to validate refresh token");
|
||||
|
|
@ -133,6 +145,10 @@ public class TokenService : ITokenService
|
|||
_logger.LogError(ex, "Failed to validate refresh token");
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_refreshTokenLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> GetJwtFromUser(AppUser user)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue