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": {