diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 732696f75..5287a124a 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API/API.csproj b/API/API.csproj index 74dabfe44..04cfd2296 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -65,19 +65,19 @@ - + - + - + @@ -96,14 +96,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 7cfa53f74..ab8c19d10 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -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()); } diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 076f7d4d9..966b37177 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -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 /// /// [HttpGet("series-detail-plus")] - public async Task> GetKavitaPlusSeriesDetailData(int seriesId) + public async Task> 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(cacheKey); + var results = await _cacheProvider.GetAsync(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(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(cacheKey)).Value; - await PrepareSeriesDetail(userReviews, newCacheResult, user); + var newCacheResult = (await _cacheProvider.GetAsync(cacheKey, cancellationToken)).Value; + await PrepareSeriesDetail(userReviews, newCacheResult); return Ok(newCacheResult); } - private async Task PrepareSeriesDetail(List userReviews, SeriesDetailPlusDto ret, AppUser user) + private async Task PrepareSeriesDetail(List 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(); } + + if (ret.Recommendations != null) + { + ret.Recommendations.OwnedSeries ??= new List(); + await unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, ret.Recommendations.OwnedSeries); + } } } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index b73c9d737..4c4ddfd12 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -144,7 +144,7 @@ public sealed class DataContext : IdentityDbContext 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"); } } diff --git a/API/Data/ManualMigrations/MigrateManualHistory.cs b/API/Data/ManualMigrations/MigrateManualHistory.cs index 6b1b11a6c..be41f0992 100644 --- a/API/Data/ManualMigrations/MigrateManualHistory.cs +++ b/API/Data/ManualMigrations/MigrateManualHistory.cs @@ -14,9 +14,6 @@ public static class MigrateManualHistory { public static async Task Migrate(DataContext dataContext, ILogger 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", diff --git a/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs b/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs index d4220e7f7..290bd0dc9 100644 --- a/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs +++ b/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs @@ -14,9 +14,9 @@ public static class MigrateUserLibrarySideNavStream { public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger 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) { diff --git a/API/Data/ManualMigrations/MigrateVolumeNumber.cs b/API/Data/ManualMigrations/MigrateVolumeNumber.cs index 4df4e29af..cae2e7f3c 100644 --- a/API/Data/ManualMigrations/MigrateVolumeNumber.cs +++ b/API/Data/ManualMigrations/MigrateVolumeNumber.cs @@ -15,8 +15,6 @@ public static class MigrateVolumeNumber { public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger 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) { diff --git a/API/Data/ManualMigrations/MigrateWantToReadExport.cs b/API/Data/ManualMigrations/MigrateWantToReadExport.cs index 1797a3b1d..8b9b8eb35 100644 --- a/API/Data/ManualMigrations/MigrateWantToReadExport.cs +++ b/API/Data/ManualMigrations/MigrateWantToReadExport.cs @@ -18,9 +18,6 @@ public static class MigrateWantToReadExport { public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger 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;"; diff --git a/API/Data/ManualMigrations/MigrateWantToReadImport.cs b/API/Data/ManualMigrations/MigrateWantToReadImport.cs index 0a3e87d35..01982e58f 100644 --- a/API/Data/ManualMigrations/MigrateWantToReadImport.cs +++ b/API/Data/ManualMigrations/MigrateWantToReadImport.cs @@ -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 diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs index 341997d86..a8d40df44 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -26,9 +26,9 @@ public interface IExternalSeriesMetadataRepository void Remove(IEnumerable? reviews); void Remove(IEnumerable? ratings); void Remove(IEnumerable? recommendations); - Task GetExternalSeriesMetadata(int seriesId, int limit = 25); + Task GetExternalSeriesMetadata(int seriesId); Task ExternalSeriesMetadataNeedsRefresh(int seriesId, DateTime expireTime); - Task GetSeriesDetailPlusDto(int seriesId, int libraryId, AppUser user); + Task 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 _userManager; - public ExternalSeriesMetadataRepository(DataContext context, IMapper mapper, UserManager 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 /// /// /// - public Task GetExternalSeriesMetadata(int seriesId, int limit = 25) + public Task 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 GetSeriesDetailPlusDto(int seriesId, int libraryId, AppUser user) + public async Task GetSeriesDetailPlusDto(int seriesId) { var seriesDetailDto = await _context.ExternalSeriesMetadata .Where(m => m.SeriesId == seriesId) diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index fd3d639b6..11842009b 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -146,7 +146,7 @@ public interface ISeriesRepository Task> GetLibraryIdsForSeriesAsync(); Task> GetSeriesMetadataForIds(IEnumerable seriesIds); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true); - Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); + Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); Task 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 /// /// /// - public async Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl) + public async Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl) { var libraryIds = await _context.Library .Where(lib => lib.Type == libraryType) diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 07d36c7c1..97ef3e07b 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -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); /// /// Commits changes to the DB. Completes the open transaction. diff --git a/API/Errors/ApiException.cs b/API/Errors/ApiException.cs index d9c1a755a..60d93729c 100644 --- a/API/Errors/ApiException.cs +++ b/API/Errors/ApiException.cs @@ -2,4 +2,3 @@ #nullable enable public record ApiException(int Status, string? Message = null, string? Details = null); -#nullable disable diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index e29cc86fb..b5a17ddcd 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -105,9 +105,11 @@ public static class ApplicationServiceExtensions { services.AddDbContextPool(options => { - options.UseSqlite("Data source=config/kavita.db"); + options.UseSqlite("Data source=config/kavita.db", builder => + { + builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + }); options.EnableDetailedErrors(); - options.EnableSensitiveDataLogging(); }); } diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index 52a711d66..4a04d29a8 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -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) diff --git a/API/Program.cs b/API/Program.cs index 17423c845..548e57859 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -93,13 +93,10 @@ public class Program Task.Run(async () => { // Apply all migrations on startup - var dataContext = services.GetRequiredService(); - var directoryService = services.GetRequiredService(); - 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"); diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 3e9b02118..36ba07ddc 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -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); diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 9dd89a0d0..a1aafab24 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -48,7 +48,7 @@ internal class SeriesDetailPlusApiDto public interface IExternalMetadataService { Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId); - Task GetSeriesDetail(int userId, int seriesId); + Task 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; + } + /// /// Retrieves Metadata about a Recommended External Series /// @@ -92,15 +97,13 @@ public class ExternalMetadataService : IExternalMetadataService } - public async Task GetSeriesDetail(int userId, int seriesId) + /// + /// Returns Series Detail data from Kavita+ - Review, Recs, Ratings + /// + /// + /// + public async Task 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(); - 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 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 ProcessRecommendations(Series series, AppUser user, IEnumerable recs, ExternalSeriesMetadata externalSeriesMetadata) + private async Task ProcessRecommendations(Series series, IEnumerable 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) diff --git a/API/Services/Plus/RecommendationService.cs b/API/Services/Plus/RecommendationService.cs index 961342ccc..24cb1445b 100644 --- a/API/Services/Plus/RecommendationService.cs +++ b/API/Services/Plus/RecommendationService.cs @@ -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)); diff --git a/API/Startup.cs b/API/Startup.cs index 86bee1bca..3b872f396 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -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"; diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index 0f9f05491..a72749400 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -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 +} \ No newline at end of file diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index acae6edc8..46d804ada 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Kavita.Common/KavitaException.cs b/Kavita.Common/KavitaException.cs index b624e0111..de10d3382 100644 --- a/Kavita.Common/KavitaException.cs +++ b/Kavita.Common/KavitaException.cs @@ -6,7 +6,6 @@ namespace Kavita.Common; /// /// These are used for errors to send to the UI that should not be reported to Sentry /// -[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) - { } } diff --git a/Kavita.Common/KavitaUnauthenticatedUserException.cs b/Kavita.Common/KavitaUnauthenticatedUserException.cs index 6cce9f981..ede20b59d 100644 --- a/Kavita.Common/KavitaUnauthenticatedUserException.cs +++ b/Kavita.Common/KavitaUnauthenticatedUserException.cs @@ -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 /// /// This will always log to Security Log -[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) - { } } diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index cedc19963..9bb6f1195 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -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(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId); + getSeriesMetadataFromPlus(seriesId: number, libraryType: LibraryType) { + return this.httpClient.get(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId + '&libraryType=' + libraryType); } getAllAgeRatings(libraries?: Array) { diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts index af026d3e6..83bfaddc2 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts @@ -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; } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 3d234c31e..2b244924f 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -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 diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index f71d0ce6e..c549aaa63 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -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": { diff --git a/openapi.json b/openapi.json index f862c6c88..d023cabb2 100644 --- a/openapi.json +++ b/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": {