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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants; using API.Constants;
using API.Data; using API.Data;
@ -196,31 +197,27 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("series-detail-plus")] [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()) var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, User.GetUserId()))
{
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))
.Where(r => !string.IsNullOrEmpty(r.Body)) .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(); .ToList();
var cacheKey = CacheKey + seriesId; var cacheKey = CacheKey + seriesId;
var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey); var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey, cancellationToken);
if (results.HasValue) if (results.HasValue)
{ {
var cachedResult = results.Value; var cachedResult = results.Value;
await PrepareSeriesDetail(userReviews, cachedResult, user); await PrepareSeriesDetail(userReviews, cachedResult);
return 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) if (ret == null)
{ {
// Cache an empty result, so we don't constantly hit K+ when we know nothing is going to resolve // 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, Recommendations = null,
Ratings = 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; var newCacheResult2 = (await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey)).Value;
await PrepareSeriesDetail(userReviews, newCacheResult2, user); await PrepareSeriesDetail(userReviews, newCacheResult2);
return Ok(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 // 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; var newCacheResult = (await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey, cancellationToken)).Value;
await PrepareSeriesDetail(userReviews, newCacheResult, user); await PrepareSeriesDetail(userReviews, newCacheResult);
return Ok(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())); userReviews.AddRange(ReviewService.SelectSpectrumOfReviews(ret.Reviews.ToList()));
ret.Reviews = userReviews; ret.Reviews = userReviews;
@ -262,5 +261,11 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
ret.Recommendations.OwnedSeries.Select(s => s.Id), user); ret.Recommendations.OwnedSeries.Select(s => s.Id), user);
ret.Recommendations.ExternalSeries = new List<ExternalSeriesDto>(); 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); .IsUnique(false);
} }
#nullable enable
private static void OnEntityTracked(object? sender, EntityTrackedEventArgs e) private static void OnEntityTracked(object? sender, EntityTrackedEventArgs e)
{ {
if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return; 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.LastModified = DateTime.Now;
entity.LastModifiedUtc = DateTime.UtcNow; entity.LastModifiedUtc = DateTime.UtcNow;
} }
#nullable disable
private void OnSaveChanges() 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) 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"); 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) switch (library.Type)
{ {
@ -57,11 +68,14 @@ public static class MigrateLibrariesToHaveAllFileTypes
}); });
break; break;
default: 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"); 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) 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()) if (await dataContext.ManualMigrationHistory.AnyAsync())
{ {
logger.LogCritical( logger.LogCritical(
@ -24,6 +21,9 @@ public static class MigrateManualHistory
return; return;
} }
logger.LogCritical(
"Running MigrateManualHistory migration - Please be patient, this may take some time. This is not an error");
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{ {
Name = "MigrateUserLibrarySideNavStream", Name = "MigrateUserLibrarySideNavStream",

View File

@ -14,9 +14,9 @@ public static class MigrateUserLibrarySideNavStream
{ {
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger) 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)); .AnyAsync(u => u.SideNavStreams.Count > 0 && u.SideNavStreams.Any(s => s.LibraryId > 0));
if (usersWithLibraryStreams) if (usersWithLibraryStreams)
@ -25,6 +25,8 @@ public static class MigrateUserLibrarySideNavStream
return; 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); var users = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams);
foreach (var user in users) 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) 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)) if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0))
{ {
logger.LogCritical( logger.LogCritical(
@ -24,6 +22,9 @@ public static class MigrateVolumeNumber
return; return;
} }
logger.LogCritical(
"Running MigrateVolumeNumber migration - Please be patient, this may take some time. This is not an error");
// Get all volumes // Get all volumes
foreach (var volume in dataContext.Volume) 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) 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"); var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
if (File.Exists(importFile)) if (File.Exists(importFile))
{ {
@ -29,6 +26,9 @@ public static class MigrateWantToReadExport
return; 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(); await using var command = dataContext.Database.GetDbConnection().CreateCommand();
command.CommandText = "Select AppUserId, Id from Series WHERE AppUserId IS NOT NULL ORDER BY AppUserId;"; 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 importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-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)) if (!File.Exists(importFile) || File.Exists(outputFile))
{ {
logger.LogCritical( logger.LogCritical(
@ -30,6 +27,9 @@ public static class MigrateWantToReadImport
return; 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 reader = new StreamReader(importFile);
using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture); using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture);
// Read the records from the CSV file // Read the records from the CSV file

View File

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

View File

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

View File

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

View File

@ -2,4 +2,3 @@
#nullable enable #nullable enable
public record ApiException(int Status, string? Message = null, string? Details = null); 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 => 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.EnableDetailedErrors();
options.EnableSensitiveDataLogging(); options.EnableSensitiveDataLogging();
}); });
} }

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@ internal class SeriesDetailPlusApiDto
public interface IExternalMetadataService public interface IExternalMetadataService
{ {
Task<ExternalSeriesDetailDto?> GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId); 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 public class ExternalMetadataService : IExternalMetadataService
@ -68,6 +68,11 @@ public class ExternalMetadataService : IExternalMetadataService
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
} }
public static bool IsPlusEligible(LibraryType type)
{
return type != LibraryType.Comic;
}
/// <summary> /// <summary>
/// Retrieves Metadata about a Recommended External Series /// Retrieves Metadata about a Recommended External Series
/// </summary> /// </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 = var needsRefresh =
await _unitOfWork.ExternalSeriesMetadataRepository.ExternalSeriesMetadataNeedsRefresh(seriesId, await _unitOfWork.ExternalSeriesMetadataRepository.ExternalSeriesMetadataNeedsRefresh(seriesId,
DateTime.UtcNow.Subtract(_externalSeriesMetadataCache)); DateTime.UtcNow.Subtract(_externalSeriesMetadataCache));
@ -108,11 +111,16 @@ public class ExternalMetadataService : IExternalMetadataService
if (!needsRefresh) if (!needsRefresh)
{ {
// Convert into DTOs and return // Convert into DTOs and return
return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, series.LibraryId, user); return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId);
} }
try 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 license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail")
.WithHeader("Accept", "application/json") .WithHeader("Accept", "application/json")
@ -149,7 +157,7 @@ public class ExternalMetadataService : IExternalMetadataService
// Recommendations // Recommendations
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>(); 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 var extRatings = externalSeriesMetadata.ExternalRatings
.Where(r => r.AverageScore > 0) .Where(r => r.AverageScore > 0)
@ -190,18 +198,17 @@ public class ExternalMetadataService : IExternalMetadataService
private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series) private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series)
{ {
var externalSeriesMetadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId); var externalSeriesMetadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId);
if (externalSeriesMetadata == null) if (externalSeriesMetadata != null) return externalSeriesMetadata;
{
externalSeriesMetadata = new ExternalSeriesMetadata();
series.ExternalSeriesMetadata = externalSeriesMetadata;
externalSeriesMetadata.SeriesId = series.Id;
_unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata);
}
externalSeriesMetadata = new ExternalSeriesMetadata();
series.ExternalSeriesMetadata = externalSeriesMetadata;
externalSeriesMetadata.SeriesId = series.Id;
_unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata);
return 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() var recDto = new RecommendationDto()
{ {
@ -213,7 +220,7 @@ public class ExternalMetadataService : IExternalMetadataService
foreach (var rec in recs) foreach (var rec in recs)
{ {
// Find the series based on name and type and that the user has access too // 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), series.Library.Type, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId),
ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); 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.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(); 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) 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 (series != null)
{ {
if (payload.AniListId <= 0) if (payload.AniListId <= 0)

View File

@ -63,7 +63,7 @@ public class RecommendationService : IRecommendationService
foreach (var rec in recs) foreach (var rec in recs)
{ {
// Find the series based on name and type and that the user has access too // 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), series.Library.Type, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId),
ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId));

View File

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

View File

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

View File

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

View File

@ -6,7 +6,6 @@ namespace Kavita.Common;
/// <summary> /// <summary>
/// These are used for errors to send to the UI that should not be reported to Sentry /// These are used for errors to send to the UI that should not be reported to Sentry
/// </summary> /// </summary>
[Serializable]
public class KavitaException : Exception public class KavitaException : Exception
{ {
public KavitaException() public KavitaException()
@ -17,8 +16,4 @@ public class KavitaException : Exception
public KavitaException(string message, Exception inner) public KavitaException(string message, Exception inner)
: base(message, 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 /// The user does not exist (aka unauthorized). This will be caught by middleware and Unauthorized() returned to UI
/// </summary> /// </summary>
/// <remarks>This will always log to Security Log</remarks> /// <remarks>This will always log to Security Log</remarks>
[Serializable]
public class KavitaUnauthenticatedUserException : Exception public class KavitaUnauthenticatedUserException : Exception
{ {
public KavitaUnauthenticatedUserException() public KavitaUnauthenticatedUserException()
@ -18,8 +17,4 @@ public class KavitaUnauthenticatedUserException : Exception
public KavitaUnauthenticatedUserException(string message, Exception inner) public KavitaUnauthenticatedUserException(string message, Exception inner)
: base(message, 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 {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
import {FilterStatement} from "../_models/metadata/v2/filter-statement"; import {FilterStatement} from "../_models/metadata/v2/filter-statement";
import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus"; import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus";
import {LibraryType} from "../_models/library/library";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -28,8 +29,8 @@ export class MetadataService {
constructor(private httpClient: HttpClient) { } constructor(private httpClient: HttpClient) { }
getSeriesMetadataFromPlus(seriesId: number) { getSeriesMetadataFromPlus(seriesId: number, libraryType: LibraryType) {
return this.httpClient.get<SeriesDetailPlus | null>(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId); return this.httpClient.get<SeriesDetailPlus | null>(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId + '&libraryType=' + libraryType);
} }
getAllAgeRatings(libraries?: Array<number>) { getAllAgeRatings(libraries?: Array<number>) {

View File

@ -47,7 +47,7 @@ import {
SideNavCompanionBarComponent SideNavCompanionBarComponent
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; 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 {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
@ -266,6 +266,12 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
case(Action.Edit): case(Action.Edit):
this.openEditCollectionTagModal(this.collectionTag); this.openEditCollectionTagModal(this.collectionTag);
break; 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: default:
break; break;
} }

View File

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

View File

@ -2059,7 +2059,8 @@
"smart-filter-deleted": "Smart Filter Deleted", "smart-filter-deleted": "Smart Filter Deleted",
"smart-filter-updated": "Created/Updated smart filter", "smart-filter-updated": "Created/Updated smart filter",
"external-source-already-exists": "An External Source already exists with the same Name/Host/API Key", "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": { "actionable": {

View File

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