More Bugfixes (#2685)

This commit is contained in:
Joe Milazzo 2024-02-03 11:46:04 -06:00 committed by GitHub
parent 4a9519b6dc
commit 061b363f96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 162 additions and 119 deletions

View File

@ -9,8 +9,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.4" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.4" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.15" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.15" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -65,19 +65,19 @@
<PackageReference Include="ExCSS" Version="4.2.4" />
<PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hangfire" Version="1.8.7" />
<PackageReference Include="Hangfire" Version="1.8.9" />
<PackageReference Include="Hangfire.InMemory" Version="0.7.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.57" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.7" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
@ -96,14 +96,14 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.36.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.18.0.83559">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.19.0.84025">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.2.0" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.4" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.0" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.15" />
<PackageReference Include="System.Drawing.Common" Version="8.0.1" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup>

View File

@ -188,12 +188,14 @@ public class AccountController : BaseApiController
{
user = await _userManager.Users
.Include(u => u.UserPreferences)
.AsSplitQuery()
.SingleOrDefaultAsync(x => x.ApiKey == loginDto.ApiKey);
}
else
{
user = await _userManager.Users
.Include(u => u.UserPreferences)
.AsSplitQuery()
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpperInvariant());
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
@ -196,31 +197,27 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("series-detail-plus")]
public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId)
public async Task<ActionResult<SeriesDetailPlusDto>> GetKavitaPlusSeriesDetailData(int seriesId, LibraryType libraryType, CancellationToken cancellationToken)
{
if (!await licenseService.HasActiveLicense())
{
return Ok(null);
}
var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
if (user == null) return Unauthorized();
var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, user.Id))
var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, User.GetUserId()))
.Where(r => !string.IsNullOrEmpty(r.Body))
.OrderByDescending(review => review.Username.Equals(user.UserName) ? 1 : 0)
.OrderByDescending(review => review.Username.Equals(User.GetUsername()) ? 1 : 0)
.ToList();
var cacheKey = CacheKey + seriesId;
var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey);
var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey, cancellationToken);
if (results.HasValue)
{
var cachedResult = results.Value;
await PrepareSeriesDetail(userReviews, cachedResult, user);
await PrepareSeriesDetail(userReviews, cachedResult);
return cachedResult;
}
var ret = await metadataService.GetSeriesDetail(user.Id, seriesId);
SeriesDetailPlusDto? ret = null;
if (ExternalMetadataService.IsPlusEligible(libraryType) && await licenseService.HasActiveLicense())
{
ret = await metadataService.GetSeriesDetailPlus(seriesId);
}
if (ret == null)
{
// Cache an empty result, so we don't constantly hit K+ when we know nothing is going to resolve
@ -230,27 +227,29 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
Recommendations = null,
Ratings = null
};
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(48));
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(48), cancellationToken);
var newCacheResult2 = (await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey)).Value;
await PrepareSeriesDetail(userReviews, newCacheResult2, user);
await PrepareSeriesDetail(userReviews, newCacheResult2);
return Ok(newCacheResult2);
}
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(48));
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(48), cancellationToken);
// For some reason if we don't use a different instance, the cache keeps changes made below
var newCacheResult = (await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey)).Value;
await PrepareSeriesDetail(userReviews, newCacheResult, user);
var newCacheResult = (await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey, cancellationToken)).Value;
await PrepareSeriesDetail(userReviews, newCacheResult);
return Ok(newCacheResult);
}
private async Task PrepareSeriesDetail(List<UserReviewDto> userReviews, SeriesDetailPlusDto ret, AppUser user)
private async Task PrepareSeriesDetail(List<UserReviewDto> userReviews, SeriesDetailPlusDto ret)
{
var isAdmin = await unitOfWork.UserRepository.IsUserAdminAsync(user);
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId())!;
userReviews.AddRange(ReviewService.SelectSpectrumOfReviews(ret.Reviews.ToList()));
ret.Reviews = userReviews;
@ -262,5 +261,11 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
ret.Recommendations.OwnedSeries.Select(s => s.Id), user);
ret.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
}
if (ret.Recommendations != null)
{
ret.Recommendations.OwnedSeries ??= new List<SeriesDto>();
await unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, ret.Recommendations.OwnedSeries);
}
}
}

View File

@ -144,7 +144,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
.IsUnique(false);
}
#nullable enable
private static void OnEntityTracked(object? sender, EntityTrackedEventArgs e)
{
if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return;
@ -161,6 +161,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
entity.LastModified = DateTime.Now;
entity.LastModifiedUtc = DateTime.UtcNow;
}
#nullable disable
private void OnSaveChanges()
{

View File

@ -15,9 +15,20 @@ public static class MigrateLibrariesToHaveAllFileTypes
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.Library.AnyAsync(l => l.LibraryFileTypes.Count == 0))
{
logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error");
return;
}
logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Please be patient, this may take some time. This is not an error");
var allLibs = await dataContext.Library.Include(l => l.LibraryFileTypes).ToListAsync();
foreach (var library in allLibs.Where(library => library.LibraryFileTypes.Count == 0))
var allLibs = await dataContext.Library
.Include(l => l.LibraryFileTypes)
.Where(library => library.LibraryFileTypes.Count == 0)
.ToListAsync();
foreach (var library in allLibs)
{
switch (library.Type)
{
@ -57,11 +68,14 @@ public static class MigrateLibrariesToHaveAllFileTypes
});
break;
default:
throw new ArgumentOutOfRangeException();
break;
}
}
await dataContext.SaveChangesAsync();
if (unitOfWork.HasChanges())
{
await dataContext.SaveChangesAsync();
}
logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error");
}
}

View File

@ -14,9 +14,6 @@ public static class MigrateManualHistory
{
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
{
logger.LogCritical(
"Running MigrateManualHistory migration - Please be patient, this may take some time. This is not an error");
if (await dataContext.ManualMigrationHistory.AnyAsync())
{
logger.LogCritical(
@ -24,6 +21,9 @@ public static class MigrateManualHistory
return;
}
logger.LogCritical(
"Running MigrateManualHistory migration - Please be patient, this may take some time. This is not an error");
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateUserLibrarySideNavStream",

View File

@ -14,9 +14,9 @@ public static class MigrateUserLibrarySideNavStream
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error");
var usersWithLibraryStreams = await dataContext.AppUser.Include(u => u.SideNavStreams)
var usersWithLibraryStreams = await dataContext.AppUser
.Include(u => u.SideNavStreams)
.AnyAsync(u => u.SideNavStreams.Count > 0 && u.SideNavStreams.Any(s => s.LibraryId > 0));
if (usersWithLibraryStreams)
@ -25,6 +25,8 @@ public static class MigrateUserLibrarySideNavStream
return;
}
logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error");
var users = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams);
foreach (var user in users)
{

View File

@ -15,8 +15,6 @@ public static class MigrateVolumeNumber
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
logger.LogCritical(
"Running MigrateVolumeNumber migration - Please be patient, this may take some time. This is not an error");
if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0))
{
logger.LogCritical(
@ -24,6 +22,9 @@ public static class MigrateVolumeNumber
return;
}
logger.LogCritical(
"Running MigrateVolumeNumber migration - Please be patient, this may take some time. This is not an error");
// Get all volumes
foreach (var volume in dataContext.Volume)
{

View File

@ -18,9 +18,6 @@ public static class MigrateWantToReadExport
{
public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger<Program> logger)
{
logger.LogCritical(
"Running MigrateWantToReadExport migration - Please be patient, this may take some time. This is not an error");
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
if (File.Exists(importFile))
{
@ -29,6 +26,9 @@ public static class MigrateWantToReadExport
return;
}
logger.LogCritical(
"Running MigrateWantToReadExport migration - Please be patient, this may take some time. This is not an error");
await using var command = dataContext.Database.GetDbConnection().CreateCommand();
command.CommandText = "Select AppUserId, Id from Series WHERE AppUserId IS NOT NULL ORDER BY AppUserId;";

View File

@ -20,9 +20,6 @@ public static class MigrateWantToReadImport
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv");
logger.LogCritical(
"Running MigrateWantToReadImport migration - Please be patient, this may take some time. This is not an error");
if (!File.Exists(importFile) || File.Exists(outputFile))
{
logger.LogCritical(
@ -30,6 +27,9 @@ public static class MigrateWantToReadImport
return;
}
logger.LogCritical(
"Running MigrateWantToReadImport migration - Please be patient, this may take some time. This is not an error");
using var reader = new StreamReader(importFile);
using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture);
// Read the records from the CSV file

View File

@ -26,9 +26,9 @@ public interface IExternalSeriesMetadataRepository
void Remove(IEnumerable<ExternalReview>? reviews);
void Remove(IEnumerable<ExternalRating>? ratings);
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId, int limit = 25);
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId, DateTime expireTime);
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId, int libraryId, AppUser user);
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
Task LinkRecommendationsToSeries(Series series);
}
@ -36,13 +36,11 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
{
private readonly DataContext _context;
private readonly IMapper _mapper;
private readonly UserManager<AppUser> _userManager;
public ExternalSeriesMetadataRepository(DataContext context, IMapper mapper, UserManager<AppUser> userManager)
public ExternalSeriesMetadataRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
_userManager = userManager;
}
public void Attach(ExternalSeriesMetadata metadata)
@ -83,13 +81,13 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId, int limit = 25)
public Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId)
{
return _context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
.Include(s => s.ExternalReviews.Take(limit))
.Include(s => s.ExternalRatings.OrderBy(r => r.AverageScore).Take(limit))
.Include(s => s.ExternalRecommendations.OrderBy(r => r.Id).Take(limit))
.Include(s => s.ExternalReviews)
.Include(s => s.ExternalRatings.OrderBy(r => r.AverageScore))
.Include(s => s.ExternalRecommendations.OrderBy(r => r.Id))
.AsSplitQuery()
.FirstOrDefaultAsync();
}
@ -102,7 +100,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
return row == null || row.LastUpdatedUtc <= expireTime;
}
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId, int libraryId, AppUser user)
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId)
{
var seriesDetailDto = await _context.ExternalSeriesMetadata
.Where(m => m.SeriesId == seriesId)

View File

@ -146,7 +146,7 @@ public interface ISeriesRepository
Task<IDictionary<int, int>> GetLibraryIdsForSeriesAsync();
Task<IList<SeriesMetadataDto>> GetSeriesMetadataForIds(IEnumerable<int> seriesIds);
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIds(IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
Task<int> GetAverageUserRating(int seriesId, int userId);
Task RemoveFromOnDeck(int seriesId, int userId);
Task ClearOnDeckRemoval(int seriesId, int userId);
@ -1918,7 +1918,7 @@ public class SeriesRepository : ISeriesRepository
/// <param name="userId"></param>
/// <param name="names"></param>
/// <returns></returns>
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIds(IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
{
var libraryIds = await _context.Library
.Where(lib => lib.Type == libraryType)

View File

@ -73,7 +73,7 @@ public class UnitOfWork : IUnitOfWork
public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper);
public IAppUserSmartFilterRepository AppUserSmartFilterRepository => new AppUserSmartFilterRepository(_context, _mapper);
public IAppUserExternalSourceRepository AppUserExternalSourceRepository => new AppUserExternalSourceRepository(_context, _mapper);
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository => new ExternalSeriesMetadataRepository(_context, _mapper, _userManager);
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository => new ExternalSeriesMetadataRepository(_context, _mapper);
/// <summary>
/// Commits changes to the DB. Completes the open transaction.

View File

@ -2,4 +2,3 @@
#nullable enable
public record ApiException(int Status, string? Message = null, string? Details = null);
#nullable disable

View File

@ -105,9 +105,11 @@ public static class ApplicationServiceExtensions
{
services.AddDbContextPool<DataContext>(options =>
{
options.UseSqlite("Data source=config/kavita.db");
options.UseSqlite("Data source=config/kavita.db", builder =>
{
builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
});
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging();
});
}

View File

@ -246,6 +246,7 @@ public static class SeriesFilter
.Where(p => p != null && p.AppUserId == userId)
.Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0) * 100)
})
.AsSplitQuery()
.AsEnumerable();
switch (comparison)
@ -300,6 +301,7 @@ public static class SeriesFilter
Series = s,
AverageRating = s.ExternalSeriesMetadata.AverageExternalRating
})
.AsSplitQuery()
.AsEnumerable();
switch (comparison)
@ -358,6 +360,7 @@ public static class SeriesFilter
.Max()
})
.Where(s => s.MaxDate != null)
.AsSplitQuery()
.AsEnumerable();
switch (comparison)

View File

@ -93,13 +93,10 @@ public class Program
Task.Run(async () =>
{
// Apply all migrations on startup
var dataContext = services.GetRequiredService<DataContext>();
var directoryService = services.GetRequiredService<IDirectoryService>();
logger.LogInformation("Running Migrations");
// v0.7.14
await MigrateWantToReadExport.Migrate(dataContext, directoryService, logger);
await MigrateWantToReadExport.Migrate(context, directoryService, logger);
await unitOfWork.CommitAsync();
logger.LogInformation("Running Migrations - complete");

View File

@ -458,6 +458,7 @@ public class ImageService : IImageService
for (var i = 0; i < coverImages.Count; i++)
{
if (!File.Exists(coverImages[i])) continue;
var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential);
tile = tile.ThumbnailImage(thumbnailWidth, height: thumbnailHeight);

View File

@ -48,7 +48,7 @@ internal class SeriesDetailPlusApiDto
public interface IExternalMetadataService
{
Task<ExternalSeriesDetailDto?> GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId);
Task<SeriesDetailPlusDto?> GetSeriesDetail(int userId, int seriesId);
Task<SeriesDetailPlusDto?> GetSeriesDetailPlus(int seriesId);
}
public class ExternalMetadataService : IExternalMetadataService
@ -68,6 +68,11 @@ public class ExternalMetadataService : IExternalMetadataService
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
}
public static bool IsPlusEligible(LibraryType type)
{
return type != LibraryType.Comic;
}
/// <summary>
/// Retrieves Metadata about a Recommended External Series
/// </summary>
@ -92,15 +97,13 @@ public class ExternalMetadataService : IExternalMetadataService
}
public async Task<SeriesDetailPlusDto?> GetSeriesDetail(int userId, int seriesId)
/// <summary>
/// Returns Series Detail data from Kavita+ - Review, Recs, Ratings
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlus(int seriesId)
{
var series =
await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId,
SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Volumes | SeriesIncludes.Chapters);
if (series == null || series.Library.Type == LibraryType.Comic) return null;
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return null;
var needsRefresh =
await _unitOfWork.ExternalSeriesMetadataRepository.ExternalSeriesMetadataNeedsRefresh(seriesId,
DateTime.UtcNow.Subtract(_externalSeriesMetadataCache));
@ -108,11 +111,16 @@ public class ExternalMetadataService : IExternalMetadataService
if (!needsRefresh)
{
// Convert into DTOs and return
return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, series.LibraryId, user);
return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId);
}
try
{
var series =
await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId,
SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Volumes | SeriesIncludes.Chapters);
if (series == null || series.Library.Type == LibraryType.Comic) return null;
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail")
.WithHeader("Accept", "application/json")
@ -149,7 +157,7 @@ public class ExternalMetadataService : IExternalMetadataService
// Recommendations
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>();
var recs = await ProcessRecommendations(series, user, result.Recommendations, externalSeriesMetadata);
var recs = await ProcessRecommendations(series, result.Recommendations, externalSeriesMetadata);
var extRatings = externalSeriesMetadata.ExternalRatings
.Where(r => r.AverageScore > 0)
@ -190,18 +198,17 @@ public class ExternalMetadataService : IExternalMetadataService
private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series)
{
var externalSeriesMetadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId);
if (externalSeriesMetadata == null)
{
externalSeriesMetadata = new ExternalSeriesMetadata();
series.ExternalSeriesMetadata = externalSeriesMetadata;
externalSeriesMetadata.SeriesId = series.Id;
_unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata);
}
if (externalSeriesMetadata != null) return externalSeriesMetadata;
externalSeriesMetadata = new ExternalSeriesMetadata();
series.ExternalSeriesMetadata = externalSeriesMetadata;
externalSeriesMetadata.SeriesId = series.Id;
_unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata);
return externalSeriesMetadata;
}
private async Task<RecommendationDto> ProcessRecommendations(Series series, AppUser user, IEnumerable<MediaRecommendationDto> recs, ExternalSeriesMetadata externalSeriesMetadata)
private async Task<RecommendationDto> ProcessRecommendations(Series series, IEnumerable<MediaRecommendationDto> recs,
ExternalSeriesMetadata externalSeriesMetadata)
{
var recDto = new RecommendationDto()
{
@ -213,7 +220,7 @@ public class ExternalMetadataService : IExternalMetadataService
foreach (var rec in recs)
{
// Find the series based on name and type and that the user has access too
var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIdsForUser(user.Id, rec.RecommendationNames,
var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIds(rec.RecommendationNames,
series.Library.Type, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId),
ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId));
@ -258,8 +265,6 @@ public class ExternalMetadataService : IExternalMetadataService
});
}
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, recDto.OwnedSeries);
recDto.OwnedSeries = recDto.OwnedSeries.DistinctBy(s => s.Id).OrderBy(r => r.Name).ToList();
recDto.ExternalSeries = recDto.ExternalSeries.DistinctBy(s => s.Name.ToNormalized()).OrderBy(r => r.Name).ToList();
@ -279,7 +284,8 @@ public class ExternalMetadataService : IExternalMetadataService
if (seriesId is > 0)
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value,
SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews);
if (series != null)
{
if (payload.AniListId <= 0)

View File

@ -63,7 +63,7 @@ public class RecommendationService : IRecommendationService
foreach (var rec in recs)
{
// Find the series based on name and type and that the user has access too
var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIdsForUser(userId, rec.RecommendationNames,
var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIds(rec.RecommendationNames,
series.Library.Type, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId),
ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId));

View File

@ -256,8 +256,8 @@ public class Startup
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
installVersion.Value = BuildInfo.Version.ToString();
unitOfWork.SettingsRepository.Update(installVersion);
await unitOfWork.CommitAsync();
logger.LogInformation("Running Migrations - complete");
}).GetAwaiter()
.GetResult();
@ -357,15 +357,13 @@ public class Startup
opts.IncludeQueryInRequestPath = true;
});
var allowIframing = Configuration.AllowIFraming;
app.Use(async (context, next) =>
{
context.Response.Headers[HeaderNames.Vary] =
new[] { "Accept-Encoding" };
if (!allowIframing)
if (!Configuration.AllowIFraming)
{
// Don't let the site be iframed outside the same origin (clickjacking)
context.Response.Headers.XFrameOptions = "SAMEORIGIN";

View File

@ -2,6 +2,7 @@
"TokenKey": "super secret unguessable key that is longer because we require it",
"Port": 5000,
"IpAddresses": "",
"BaseUrl": "/test/",
"Cache": 90
}
"BaseUrl": "/",
"Cache": 90,
"AllowIFraming": false
}

View File

@ -15,7 +15,7 @@
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.18.0.83559">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.19.0.84025">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -6,7 +6,6 @@ namespace Kavita.Common;
/// <summary>
/// These are used for errors to send to the UI that should not be reported to Sentry
/// </summary>
[Serializable]
public class KavitaException : Exception
{
public KavitaException()
@ -17,8 +16,4 @@ public class KavitaException : Exception
public KavitaException(string message, Exception inner)
: base(message, inner) { }
protected KavitaException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}

View File

@ -7,7 +7,6 @@ namespace Kavita.Common;
/// The user does not exist (aka unauthorized). This will be caught by middleware and Unauthorized() returned to UI
/// </summary>
/// <remarks>This will always log to Security Log</remarks>
[Serializable]
public class KavitaUnauthenticatedUserException : Exception
{
public KavitaUnauthenticatedUserException()
@ -18,8 +17,4 @@ public class KavitaUnauthenticatedUserException : Exception
public KavitaUnauthenticatedUserException(string message, Exception inner)
: base(message, inner) { }
protected KavitaUnauthenticatedUserException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}

View File

@ -17,6 +17,7 @@ import {FilterCombination} from "../_models/metadata/v2/filter-combination";
import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
import {FilterStatement} from "../_models/metadata/v2/filter-statement";
import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus";
import {LibraryType} from "../_models/library/library";
@Injectable({
providedIn: 'root'
@ -28,8 +29,8 @@ export class MetadataService {
constructor(private httpClient: HttpClient) { }
getSeriesMetadataFromPlus(seriesId: number) {
return this.httpClient.get<SeriesDetailPlus | null>(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId);
getSeriesMetadataFromPlus(seriesId: number, libraryType: LibraryType) {
return this.httpClient.get<SeriesDetailPlus | null>(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId + '&libraryType=' + libraryType);
}
getAllAgeRatings(libraries?: Array<number>) {

View File

@ -47,7 +47,7 @@ import {
SideNavCompanionBarComponent
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
@ -266,6 +266,12 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
case(Action.Edit):
this.openEditCollectionTagModal(this.collectionTag);
break;
case (Action.Delete):
this.collectionService.deleteTag(this.collectionTag.id).subscribe(() => {
this.toastr.success(translate('toasts.collection-tag-deleted'));
this.router.navigateByUrl('collections');
});
break;
default:
break;
}

View File

@ -598,9 +598,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
});
this.setContinuePoint();
if (loadExternal) {
this.loadPlusMetadata(this.seriesId);
}
forkJoin({
libType: this.libraryService.getLibraryType(this.libraryId),
@ -609,6 +606,10 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.libraryType = results.libType;
this.series = results.series;
if (loadExternal) {
this.loadPlusMetadata(this.seriesId, this.libraryType);
}
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
@ -706,8 +707,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
}
loadPlusMetadata(seriesId: number) {
this.metadataService.getSeriesMetadataFromPlus(seriesId).subscribe(data => {
loadPlusMetadata(seriesId: number, libraryType: LibraryType) {
this.metadataService.getSeriesMetadataFromPlus(seriesId, libraryType).subscribe(data => {
if (data === null) return;
// Reviews

View File

@ -2059,7 +2059,8 @@
"smart-filter-deleted": "Smart Filter Deleted",
"smart-filter-updated": "Created/Updated smart filter",
"external-source-already-exists": "An External Source already exists with the same Name/Host/API Key",
"anilist-token-expired": "Your AniList token is expired. Scrobbling will no longer process until you re-generate it in User Settings > Account"
"anilist-token-expired": "Your AniList token is expired. Scrobbling will no longer process until you re-generate it in User Settings > Account",
"collection-tag-deleted": "Collection Tag deleted"
},
"actionable": {

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.13.16"
"version": "0.7.13.17"
},
"servers": [
{
@ -3581,6 +3581,20 @@
"type": "integer",
"format": "int32"
}
},
{
"name": "libraryType",
"in": "query",
"schema": {
"enum": [
0,
1,
2,
3
],
"type": "integer",
"format": "int32"
}
}
],
"responses": {