mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
More Bugfixes (#2685)
This commit is contained in:
parent
4a9519b6dc
commit
061b363f96
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;";
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -2,4 +2,3 @@
|
||||
|
||||
#nullable enable
|
||||
public record ApiException(int Status, string? Message = null, string? Details = null);
|
||||
#nullable disable
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
}
|
@ -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>
|
||||
|
@ -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)
|
||||
{ }
|
||||
}
|
||||
|
@ -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)
|
||||
{ }
|
||||
}
|
||||
|
@ -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>) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
16
openapi.json
16
openapi.json
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user