Bugfixes + Potential iOS Webtoon Reader Fix (#2650)

This commit is contained in:
Joe Milazzo 2024-01-25 11:09:44 -06:00 committed by GitHub
parent 56fa393cf0
commit f660a1cd06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 157 additions and 197 deletions

View File

@ -1,18 +1,20 @@
using API.Helpers.Converters;
using Hangfire;
using Xunit;
namespace API.Tests.Converters;
#nullable enable
public class CronConverterTests
{
[Theory]
[InlineData("daily", "0 0 * * *")]
[InlineData("disabled", "0 0 31 2 *")]
[InlineData("weekly", "0 0 * * 1")]
[InlineData("", "0 0 31 2 *")]
[InlineData("0 0 31 2 *", "0 0 31 2 *")]
[InlineData("sdfgdf", "sdfgdf")]
[InlineData("* * * * *", "* * * * *")]
public void ConvertTest(string input, string expected)
[InlineData(null, "0 0 * * *")] // daily
public void ConvertTest(string? input, string expected)
{
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
}

View File

@ -293,6 +293,7 @@ public class MangaParserTests
[InlineData("Bleach 001-003", "1-3")]
[InlineData("Accel World Volume 2", "0")]
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
[InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")]
public void ParseChaptersTest(string filename, string expected)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));

View File

@ -142,7 +142,8 @@ public class DeviceController : BaseApiController
if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId"));
if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId"));
if (await _emailService.IsDefaultEmailService())
var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetup();
if (!isEmailSetup)
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
var userId = User.GetUserId();

View File

@ -8,6 +8,7 @@ using API.Data;
using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.DTOs.Recommendation;
using API.DTOs.SeriesDetail;
using API.Entities.Enums;
using API.Extensions;
@ -207,13 +208,18 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
.OrderByDescending(review => review.Username.Equals(user.UserName) ? 1 : 0)
.ToList();
var cacheKey = CacheKey + seriesId + "_" + user.Id;
var cacheKey = CacheKey + seriesId;
var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey);
if (results.HasValue)
{
var cachedResult = results.Value;
userReviews.AddRange(cachedResult.Reviews);
cachedResult.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
if (!await unitOfWork.UserRepository.IsUserAdminAsync(user))
{
cachedResult.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
}
return cachedResult;
}
@ -221,10 +227,13 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
if (ret == null) return Ok(null);
userReviews.AddRange(ret.Reviews);
ret.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(24));
if (!await unitOfWork.UserRepository.IsUserAdminAsync(user))
{
ret.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
}
return Ok(ret);
}

View File

@ -189,6 +189,15 @@ public class ServerController : BaseApiController
return Ok(await _versionUpdaterService.CheckForUpdate());
}
/// <summary>
/// Returns how many versions out of date this install is
/// </summary>
[HttpGet("check-out-of-date")]
public async Task<ActionResult<int>> CheckHowOutOfDate()
{
return Ok(await _versionUpdaterService.GetNumberOfReleasesBehind());
}
/// <summary>
/// Pull the Changelog for Kavita from Github and display
@ -260,7 +269,7 @@ public class ServerController : BaseApiController
/// </summary>
/// <returns></returns>
[Authorize("RequireAdminRole")]
[HttpPost("bust-review-and-rec-cache")]
[HttpPost("bust-kavitaplus-cache")]
public async Task<ActionResult> BustReviewAndRecCache()
{
_logger.LogInformation("Busting Kavita+ Cache");

View File

@ -124,19 +124,6 @@ public class SettingsController : BaseApiController
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
}
/// <summary>
/// Sends a test email from the Email Service.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("test-email-url")]
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
return Ok(await _emailService.SendTestEmail(user!.Email));
}
/// <summary>
/// Is the minimum information setup for Email to work
/// </summary>

View File

@ -85,9 +85,9 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
{
return _context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
.Include(s => s.ExternalReviews.Take(25))
.Include(s => s.ExternalRatings.Take(25))
.Include(s => s.ExternalRecommendations.Take(25))
.Include(s => s.ExternalReviews.Take(limit))
.Include(s => s.ExternalRatings.Take(limit))
.Include(s => s.ExternalRecommendations.Take(limit))
.AsSplitQuery()
.FirstOrDefaultAsync();
}
@ -138,7 +138,13 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
var seriesDetailPlusDto = new SeriesDetailPlusDto()
{
Ratings = seriesDetailDto.ExternalRatings.Select(r => _mapper.Map<RatingDto>(r)),
Reviews = seriesDetailDto.ExternalReviews.OrderByDescending(r => r.Score).Select(r => _mapper.Map<UserReviewDto>(r)),
Reviews = seriesDetailDto.ExternalReviews.OrderByDescending(r => r.Score)
.Select(r =>
{
var ret = _mapper.Map<UserReviewDto>(r);
ret.IsExternal = true;
return ret;
}),
Recommendations = new RecommendationDto()
{
ExternalSeries = externalSeriesRecommendations,

View File

@ -490,6 +490,10 @@ public class SeriesRepository : ISeriesRepository
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
else
{
result.Files = new List<MangaFileDto>();
}
result.Chapters = await _context.Chapter
.Include(c => c.Files)
@ -1930,7 +1934,8 @@ public class SeriesRepository : ISeriesRepository
{
// If there is 0 or 1 rating and that rating is you, return 0 back
var countOfRatingsThatAreUser = await _context.AppUserRating
.Where(r => r.SeriesId == seriesId && r.HasBeenRated).CountAsync(u => u.AppUserId == userId);
.Where(r => r.SeriesId == seriesId && r.HasBeenRated)
.CountAsync(u => u.AppUserId == userId);
if (countOfRatingsThatAreUser == 1)
{
return 0;

View File

@ -12,8 +12,14 @@ public static class CronConverter
"daily",
"weekly",
};
public static string ConvertToCronNotation(string source)
/// <summary>
/// Converts to Cron Notation
/// </summary>
/// <param name="source">Defaults to daily</param>
/// <returns></returns>
public static string ConvertToCronNotation(string? source)
{
if (string.IsNullOrEmpty(source)) return Cron.Daily();
return source.ToLower() switch
{
"daily" => Cron.Daily(),

View File

@ -7,12 +7,8 @@ using System.Net;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Email;
using API.Entities.Enums;
using Flurl.Http;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using MailKit.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using MimeKit;
@ -38,7 +34,6 @@ public interface IEmailService
Task<bool> SendForgotPasswordEmail(PasswordResetEmailDto dto);
Task<bool> SendFilesToEmail(SendToDto data);
Task<EmailTestResultDto> SendTestEmail(string adminEmail);
Task<bool> IsDefaultEmailService();
Task SendEmailChangeEmail(ConfirmationEmailDto data);
bool IsValidEmail(string email);
}
@ -47,7 +42,6 @@ public class EmailService : IEmailService
{
private readonly ILogger<EmailService> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly IDownloadService _downloadService;
private readonly IDirectoryService _directoryService;
private const string TemplatePath = @"{0}.html";
@ -57,11 +51,10 @@ public class EmailService : IEmailService
public const string DefaultApiUrl = "https://email.kavitareader.com";
public EmailService(ILogger<EmailService> logger, IUnitOfWork unitOfWork, IDownloadService downloadService, IDirectoryService directoryService)
public EmailService(ILogger<EmailService> logger, IUnitOfWork unitOfWork, IDirectoryService directoryService)
{
_logger = logger;
_unitOfWork = unitOfWork;
_downloadService = downloadService;
_directoryService = directoryService;
}
@ -114,14 +107,6 @@ public class EmailService : IEmailService
return result;
}
[Obsolete]
public async Task<bool> IsDefaultEmailService()
{
return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value!
.Equals(DefaultApiUrl);
}
/// <summary>
/// Sends an email that has a link that will finalize an Email Change
/// </summary>

View File

@ -36,7 +36,7 @@ internal class ExternalMetadataIdsDto
public MediaFormat? PlusMediaFormat { get; set; } = MediaFormat.Unknown;
}
internal class SeriesDetailPlusAPIDto
internal class SeriesDetailPlusApiDto
{
public IEnumerable<MediaRecommendationDto> Recommendations { get; set; }
public IEnumerable<UserReviewDto> Reviews { get; set; }
@ -108,7 +108,7 @@ public class ExternalMetadataService : IExternalMetadataService
if (!needsRefresh)
{
// Convert into DTOs and return
return await SerializeExternalSeriesDetail(seriesId, externalSeriesMetadata, user, series);
return await SerializeExternalSeriesDetail(seriesId, series.LibraryId, user);
}
try
@ -123,7 +123,7 @@ public class ExternalMetadataService : IExternalMetadataService
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
.ReceiveJson<SeriesDetailPlusAPIDto>();
.ReceiveJson<SeriesDetailPlusApiDto>();
// Clear out existing results
@ -149,7 +149,7 @@ public class ExternalMetadataService : IExternalMetadataService
// Recommendations
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>();
var recs = await ProcessRecommendations(series, user!, result.Recommendations, externalSeriesMetadata);
var recs = await ProcessRecommendations(series, user, result.Recommendations, externalSeriesMetadata);
externalSeriesMetadata.LastUpdatedUtc = DateTime.UtcNow;
externalSeriesMetadata.AverageExternalRating = (int) externalSeriesMetadata.ExternalRatings
@ -161,14 +161,12 @@ public class ExternalMetadataService : IExternalMetadataService
await _unitOfWork.CommitAsync();
var ret = new SeriesDetailPlusDto()
return new SeriesDetailPlusDto()
{
Recommendations = recs,
Ratings = result.Ratings,
Reviews = result.Reviews
};
return ret;
}
catch (FlurlHttpException ex)
{
@ -185,46 +183,9 @@ public class ExternalMetadataService : IExternalMetadataService
return null;
}
private async Task<SeriesDetailPlusDto?> SerializeExternalSeriesDetail(int seriesId, ExternalSeriesMetadata externalSeriesMetadata,
AppUser user, Series series)
private async Task<SeriesDetailPlusDto?> SerializeExternalSeriesDetail(int seriesId, int libraryId, AppUser user)
{
var seriesIdsOnServer = externalSeriesMetadata.ExternalRecommendations
.Where(r => r.SeriesId is > 0)
.Select(s => (int) s.SeriesId!)
.ToList();
var ownedSeries = (await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(seriesIdsOnServer, user.Id))
.ToList();
var canSeeExternalSeries = user is {AgeRestriction: AgeRating.NotApplicable} &&
await _unitOfWork.UserRepository.IsUserAdminAsync(user);
var externalSeries = new List<ExternalSeriesDto>();
if (canSeeExternalSeries)
{
externalSeries = externalSeriesMetadata.ExternalRecommendations
.Where(r => r.SeriesId is null or 0)
.Select(r => _mapper.Map<ExternalSeriesDto>(r))
.ToList();
}
var ret = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, series.LibraryId, user);
return new SeriesDetailPlusDto()
{
Ratings = externalSeriesMetadata.ExternalRatings.Select(r => _mapper.Map<RatingDto>(r)),
Reviews = externalSeriesMetadata.ExternalReviews.OrderByDescending(r => r.Score).Select(r =>
{
var review = _mapper.Map<UserReviewDto>(r);
review.SeriesId = seriesId;
review.LibraryId = series.LibraryId;
review.IsExternal = true;
return review;
}),
Recommendations = new RecommendationDto()
{
ExternalSeries = externalSeries,
OwnedSeries = ownedSeries
}
};
return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, libraryId, user);
}
private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series)
@ -249,8 +210,6 @@ public class ExternalMetadataService : IExternalMetadataService
OwnedSeries = new List<SeriesDto>()
};
var canSeeExternalSeries = user is {AgeRestriction: AgeRating.NotApplicable} &&
await _unitOfWork.UserRepository.IsUserAdminAsync(user);
// NOTE: This can result in a series being recommended that shares the same name but different format
foreach (var rec in recs)
{
@ -276,7 +235,6 @@ public class ExternalMetadataService : IExternalMetadataService
continue;
}
if (!canSeeExternalSeries) continue;
// We can show this based on user permissions
if (string.IsNullOrEmpty(rec.Name) || string.IsNullOrEmpty(rec.SiteUrl) || string.IsNullOrEmpty(rec.CoverUrl)) continue;
recDto.ExternalSeries.Add(new ExternalSeriesDto()

View File

@ -32,7 +32,7 @@ namespace API.Services;
public interface ISeriesService
{
Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId);
Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, int userId = 0);
Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto);
Task<bool> UpdateRating(AppUser user, UpdateSeriesRatingDto updateSeriesRatingDto);
Task<bool> DeleteMultipleSeries(IList<int> seriesIds);
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
@ -111,9 +111,8 @@ public class SeriesService : ISeriesService
/// Updates the Series Metadata.
/// </summary>
/// <param name="updateSeriesMetadataDto"></param>
/// <param name="userId">If 0, does not bust any cache</param>
/// <returns></returns>
public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, int userId = 0)
public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
{
var hasWebLinksChanged = false;
try
@ -315,10 +314,10 @@ public class SeriesService : ISeriesService
_logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work");
}
if (hasWebLinksChanged && userId > 0)
if (hasWebLinksChanged)
{
_logger.LogDebug("Clearing cache as series weblinks may have changed");
await _cacheProvider.RemoveAsync(MetadataController.CacheKey + seriesId + userId);
await _cacheProvider.RemoveAsync(MetadataController.CacheKey + seriesId);
}

View File

@ -543,7 +543,7 @@ public static class Parser
{
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
new Regex(
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-\d+(\.\d)?)?)",
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-c?\d+(\.\d)?)?)",
MatchOptions, RegexTimeout),
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
new Regex(
@ -761,6 +761,11 @@ public static class Parser
var from = RemoveLeadingZeroes(tokens[0]);
if (tokens.Length != 2) return from;
// Occasionally users will use c01-c02 instead of c01-02, clean any leftover c
if (tokens[1].StartsWith("c", StringComparison.InvariantCultureIgnoreCase))
{
tokens[1] = tokens[1].Replace("c", string.Empty, StringComparison.InvariantCultureIgnoreCase);
}
var to = RemoveLeadingZeroes(hasPart ? AddChapterPart(tokens[1]) : tokens[1]);
return $"{from}-{to}";
}

View File

@ -752,6 +752,8 @@ public class ProcessSeries : IProcessSeries
.Where(s => !string.IsNullOrEmpty(s))
.Select(s => s.Trim())
);
// For each weblink, try to parse out some MetadataIds and store in the Chapter directly for matching (CBL)
}
if (!string.IsNullOrEmpty(comicInfo.Isbn))

View File

@ -48,6 +48,7 @@ public interface IVersionUpdaterService
Task<UpdateNotificationDto?> CheckForUpdate();
Task PushUpdate(UpdateNotificationDto update);
Task<IEnumerable<UpdateNotificationDto>> GetAllReleases();
Task<int> GetNumberOfReleasesBehind();
}
public class VersionUpdaterService : IVersionUpdaterService
@ -87,6 +88,12 @@ public class VersionUpdaterService : IVersionUpdaterService
return updates.Select(CreateDto).Where(d => d != null)!;
}
public async Task<int> GetNumberOfReleasesBehind()
{
var updates = await GetAllReleases();
return updates.TakeWhile(update => update.UpdateVersion != update.CurrentVersion).Count();
}
private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update)
{
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null;

View File

@ -1 +0,0 @@
{"zh_Hant":"05191aaae25a26a8597559e8318f97db","zh_Hans":"7edb04f6c2439da2cde73996aed08029","uk":"ccf59f571821ab842882378395ccf48c","tr":"5d6427179210cc370400b816c9d1116d","th":"1e27a1e1cadb2b9f92d85952bffaab95","sk":"24de417448b577b4899e917b70a43263","ru":"c547f0995c167817dd2408e4e9279de2","pt_BR":"5acd3a08c1d9aabfae5a74a438cff79b","pt":"af4162a48f01c5260d6436e7e000c5ef","pl":"c6488fdb9a1ecfe5cde6bd1c264902aa","nl":"3ff322f7b24442bd6bceb5c692146d4f","nb_NO":"99914b932bd37a50b983c5e7c90ae93b","ms":"9fdfcc11a2e8a58a4baa691b93d93ff7","ko":"447e24f9f60e1b9f36bc0b087d059dbd","ja":"27bec4796972f0338404ebdb5829af14","it":"4ef0a0ef56bab4650eda37e0dd841982","id":"cfaff69f0a68d9b6196b6c11986508f8","hi":"d850bb49ec6b5a5ccf9986823f095ab8","fr":"c648c43f9ea0bb20ddb00c0566bbd85a","es":"5816bb68d1d64c40de890c0be0222c71","en":"9b15b7b325483ec581eca99a4bb93f93","de":"c3a4fd22b51fd5a675363a6a35d1611e","cs":"bd76bfbd0e5538378dfe99d034b2adfe"}

View File

@ -681,6 +681,7 @@
"version": "17.1.0",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.1.0.tgz",
"integrity": "sha512-WDpO4WvC5ItjaRexnpFpKPpT+cu+5GYkWF8h74iHhfxOgU+gaQiMWERHylWCqF25AzmhKu0iI3ZZtaIJ6qqwog==",
"dev": true,
"dependencies": {
"@babel/core": "7.23.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
@ -5674,6 +5675,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@ -5919,6 +5921,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -6224,6 +6227,7 @@
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
@ -6472,7 +6476,8 @@
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true
},
"node_modules/cookie": {
"version": "0.5.0",
@ -7362,6 +7367,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
@ -7371,6 +7377,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@ -8450,6 +8457,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@ -9224,6 +9232,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@ -11000,6 +11009,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -12390,6 +12400,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@ -12400,7 +12411,8 @@
"node_modules/reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
"dev": true
},
"node_modules/regenerate": {
"version": "1.4.2",
@ -12852,7 +12864,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true
"dev": true
},
"node_modules/sass": {
"version": "1.69.7",
@ -12968,6 +12980,7 @@
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@ -12982,6 +12995,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@ -12992,7 +13006,8 @@
"node_modules/semver/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/send": {
"version": "0.18.0",
@ -14099,6 +14114,7 @@
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -63,7 +63,7 @@ export class ServerService {
}
bustCache() {
return this.http.post(this.baseUrl + 'server/bust-review-and-rec-cache', {});
return this.http.post(this.baseUrl + 'server/bust-kavitaplus-cache', {});
}
getMediaErrors() {

View File

@ -45,6 +45,7 @@ export class ManageSettingsComponent implements OnInit {
this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required]));
this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required]));
this.settingsForm.addControl('taskCleanup', new FormControl(this.serverSettings.taskCleanup, [Validators.required]));
this.settingsForm.addControl('ipAddresses', new FormControl(this.serverSettings.ipAddresses, [Validators.required, Validators.pattern(ValidIpAddress)]));
this.settingsForm.addControl('port', new FormControl(this.serverSettings.port, [Validators.required]));
this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required]));
@ -77,6 +78,7 @@ export class ManageSettingsComponent implements OnInit {
this.settingsForm.get('cacheDirectory')?.setValue(this.serverSettings.cacheDirectory);
this.settingsForm.get('scanTask')?.setValue(this.serverSettings.taskScan);
this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup);
this.settingsForm.get('taskCleanup')?.setValue(this.serverSettings.taskCleanup);
this.settingsForm.get('ipAddresses')?.setValue(this.serverSettings.ipAddresses);
this.settingsForm.get('port')?.setValue(this.serverSettings.port);
this.settingsForm.get('loggingLevel')?.setValue(this.serverSettings.loggingLevel);

View File

@ -48,7 +48,7 @@
{{t('roles-title')}} <span *ngIf="roles.length === 0; else showRoles">{{null | defaultValue}}</span>
<ng-template #showRoles>
<ng-container *ngIf="hasAdminRole(member); else allRoles">
<app-tag-badge class="col-auto">Admin</app-tag-badge>
<app-tag-badge class="col-auto">{{t('admin')}}</app-tag-badge>
</ng-container>
<ng-template #allRoles>
<app-tag-badge *ngFor="let role of roles" class="col-auto">{{role}}</app-tag-badge>

View File

@ -55,7 +55,7 @@
<app-carousel-reel [items]="data" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick(StreamId.OnDeck)">
<ng-template #carouselItem let-item>
<app-series-card [data]="item" [libraryId]="item.libraryId" [isOnDeck]="true"
(reload)="reloadStream(stream.id)" (dataChanged)="reloadStream(stream.id)"></app-series-card>
(reload)="reloadStream(stream.id, true)" (dataChanged)="reloadStream(stream.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
}

View File

@ -193,12 +193,17 @@ export class DashboardComponent implements OnInit {
this.cdRef.markForCheck();
}
reloadStream(streamId: number) {
reloadStream(streamId: number, onDeck = false) {
const index = this.streams.findIndex(s => s.id === streamId);
if (index < 0) return;
this.streams[index] = {...this.streams[index]};
console.log('swapped out stream: ', this.streams[index]);
this.cdRef.detectChanges();
if (onDeck) {
// TODO: Need to figure out a better way to refresh just one stream
this.refreshStreams$.next();
this.cdRef.markForCheck();
} else {
this.streams[index] = {...this.streams[index]};
this.cdRef.markForCheck();
}
}
async handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {

View File

@ -206,9 +206,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
.subscribe((event) => this.handleScrollEvent(event));
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend')
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
.subscribe((event) => this.handleScrollEndEvent(event));
// fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend')
// .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
// .subscribe((event) => this.handleScrollEndEvent(event));
}
ngOnInit(): void {

View File

@ -558,7 +558,7 @@
"invite-user": {
"title": "Invite User",
"close": "{{common.close}}",
"description": "Invite a user to your server by entering their email. They'll receive an email to create an account. For this to work you must have the Host Name field set in the <a href=\"/admin/dashboard#email\" rel=\"noopener noreferrer\" target=\"_blank\">Email</a> tab or be accessing your instance through it's remote URL. <br/><br/>Alternatively, you can use a username, but note that password resets won't be available for username-based accounts. If you do not want to use our email service, you can host your own email service.",
"description": "Invite a user to your server by entering their email. They'll receive an email to create an account. For this to work you must have the Host Name and Email setting fields set in the <a href=\"/admin/dashboard#email\" rel=\"noopener noreferrer\" target=\"_blank\">Email</a> tab, otherwise a link will be presented to you to setup on the user's behalf.<br/><br/>The email is not required to be valid.",
"email": "{{common.email}}",
"required-field": "{{common.required-field}}",
"setup-user-title": "User invited",
@ -1223,8 +1223,8 @@
"title": "Recurring Tasks",
"library-scan-label": "Library Scan",
"library-scan-tooltip": "How often Kavita will scan and refresh metadata around library files.",
"library-database-backup-label": "Library Database Backup",
"library-database-backup-tooltip": "How often Kavita will backup the database.",
"library-database-backup-label": "Kavita Backup",
"library-database-backup-tooltip": "How often Kavita will backup the database and other related files.",
"cleanup-label": "Cleanup",
"cleanup-tooltip": "How often Kavita will run cleanup tasks. This can be heavy and should be performed at midnight in most cases",
"adhoc-tasks-title": "Ad-hoc Tasks",
@ -1237,7 +1237,7 @@
"required": "{{validation.required-field}}",
"custom-label": "Custom Schedule (Cron Notation)",
"cron-notation": "You must use cron notation for custom scheduling",
"cron-notation": "This is not valid Cron Notation",
"recurring-tasks-title": "{{title}}",
"last-executed-header": "Last Executed",
@ -1302,6 +1302,7 @@
"change-password-alt": "Change Password {{user}}",
"resend": "Resend",
"setup": "Setup",
"admin": "Admin",
"last-active-title": "Last Active:",
"roles-title": "Roles:",
"none": "None",

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.13.1"
"version": "0.7.13.2"
},
"servers": [
{
@ -9488,6 +9488,39 @@
}
}
},
"/api/Server/check-out-of-date": {
"get": {
"tags": [
"Server"
],
"summary": "Returns how many versions out of date this install is",
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "integer",
"format": "int32"
}
},
"application/json": {
"schema": {
"type": "integer",
"format": "int32"
}
},
"text/json": {
"schema": {
"type": "integer",
"format": "int32"
}
}
}
}
}
}
},
"/api/Server/changelog": {
"get": {
"tags": [
@ -9649,7 +9682,7 @@
}
}
},
"/api/Server/bust-review-and-rec-cache": {
"/api/Server/bust-kavitaplus-cache": {
"post": {
"tags": [
"Server"
@ -9869,56 +9902,6 @@
}
}
},
"/api/Settings/test-email-url": {
"post": {
"tags": [
"Settings"
],
"summary": "Sends a test email from the Email Service.",
"requestBody": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestEmailDto"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/TestEmailDto"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/TestEmailDto"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/EmailTestResultDto"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/EmailTestResultDto"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/EmailTestResultDto"
}
}
}
}
}
}
},
"/api/Settings/is-email-setup": {
"get": {
"tags": [
@ -14782,24 +14765,6 @@
},
"additionalProperties": false
},
"EmailTestResultDto": {
"type": "object",
"properties": {
"successful": {
"type": "boolean"
},
"errorMessage": {
"type": "string",
"nullable": true
},
"emailAddress": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false,
"description": "Represents if Test Email Service URL was successful or not and if any error occured"
},
"ExternalRating": {
"type": "object",
"properties": {
@ -19110,16 +19075,6 @@
},
"additionalProperties": false
},
"TestEmailDto": {
"type": "object",
"properties": {
"url": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"TokenRequestDto": {
"type": "object",
"properties": {