mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Bugfixes + Potential iOS Webtoon Reader Fix (#2650)
This commit is contained in:
parent
56fa393cf0
commit
f660a1cd06
@ -1,18 +1,20 @@
|
|||||||
using API.Helpers.Converters;
|
using API.Helpers.Converters;
|
||||||
|
using Hangfire;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Converters;
|
namespace API.Tests.Converters;
|
||||||
|
#nullable enable
|
||||||
public class CronConverterTests
|
public class CronConverterTests
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("daily", "0 0 * * *")]
|
[InlineData("daily", "0 0 * * *")]
|
||||||
[InlineData("disabled", "0 0 31 2 *")]
|
[InlineData("disabled", "0 0 31 2 *")]
|
||||||
[InlineData("weekly", "0 0 * * 1")]
|
[InlineData("weekly", "0 0 * * 1")]
|
||||||
[InlineData("", "0 0 31 2 *")]
|
[InlineData("0 0 31 2 *", "0 0 31 2 *")]
|
||||||
[InlineData("sdfgdf", "sdfgdf")]
|
[InlineData("sdfgdf", "sdfgdf")]
|
||||||
[InlineData("* * * * *", "* * * * *")]
|
[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));
|
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
|
||||||
}
|
}
|
||||||
|
@ -293,6 +293,7 @@ public class MangaParserTests
|
|||||||
[InlineData("Bleach 001-003", "1-3")]
|
[InlineData("Bleach 001-003", "1-3")]
|
||||||
[InlineData("Accel World Volume 2", "0")]
|
[InlineData("Accel World Volume 2", "0")]
|
||||||
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
|
[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)
|
public void ParseChaptersTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||||
|
@ -142,7 +142,8 @@ public class DeviceController : BaseApiController
|
|||||||
if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId"));
|
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 (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"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
|
||||||
|
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
|
@ -8,6 +8,7 @@ using API.Data;
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
|
using API.DTOs.Recommendation;
|
||||||
using API.DTOs.SeriesDetail;
|
using API.DTOs.SeriesDetail;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
@ -207,13 +208,18 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
|
|||||||
.OrderByDescending(review => review.Username.Equals(user.UserName) ? 1 : 0)
|
.OrderByDescending(review => review.Username.Equals(user.UserName) ? 1 : 0)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var cacheKey = CacheKey + seriesId + "_" + user.Id;
|
var cacheKey = CacheKey + seriesId;
|
||||||
var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey);
|
var results = await _cacheProvider.GetAsync<SeriesDetailPlusDto>(cacheKey);
|
||||||
if (results.HasValue)
|
if (results.HasValue)
|
||||||
{
|
{
|
||||||
var cachedResult = results.Value;
|
var cachedResult = results.Value;
|
||||||
userReviews.AddRange(cachedResult.Reviews);
|
userReviews.AddRange(cachedResult.Reviews);
|
||||||
cachedResult.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
|
cachedResult.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
|
||||||
|
if (!await unitOfWork.UserRepository.IsUserAdminAsync(user))
|
||||||
|
{
|
||||||
|
cachedResult.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
|
||||||
|
}
|
||||||
|
|
||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,10 +227,13 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
|
|||||||
if (ret == null) return Ok(null);
|
if (ret == null) return Ok(null);
|
||||||
userReviews.AddRange(ret.Reviews);
|
userReviews.AddRange(ret.Reviews);
|
||||||
ret.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
|
ret.Reviews = ReviewService.SelectSpectrumOfReviews(userReviews);
|
||||||
|
|
||||||
|
|
||||||
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(24));
|
await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(24));
|
||||||
|
|
||||||
|
if (!await unitOfWork.UserRepository.IsUserAdminAsync(user))
|
||||||
|
{
|
||||||
|
ret.Recommendations.ExternalSeries = new List<ExternalSeriesDto>();
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(ret);
|
return Ok(ret);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -189,6 +189,15 @@ public class ServerController : BaseApiController
|
|||||||
return Ok(await _versionUpdaterService.CheckForUpdate());
|
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>
|
/// <summary>
|
||||||
/// Pull the Changelog for Kavita from Github and display
|
/// Pull the Changelog for Kavita from Github and display
|
||||||
@ -260,7 +269,7 @@ public class ServerController : BaseApiController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Authorize("RequireAdminRole")]
|
[Authorize("RequireAdminRole")]
|
||||||
[HttpPost("bust-review-and-rec-cache")]
|
[HttpPost("bust-kavitaplus-cache")]
|
||||||
public async Task<ActionResult> BustReviewAndRecCache()
|
public async Task<ActionResult> BustReviewAndRecCache()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Busting Kavita+ Cache");
|
_logger.LogInformation("Busting Kavita+ Cache");
|
||||||
|
@ -124,19 +124,6 @@ public class SettingsController : BaseApiController
|
|||||||
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
|
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>
|
/// <summary>
|
||||||
/// Is the minimum information setup for Email to work
|
/// Is the minimum information setup for Email to work
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -85,9 +85,9 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
{
|
{
|
||||||
return _context.ExternalSeriesMetadata
|
return _context.ExternalSeriesMetadata
|
||||||
.Where(s => s.SeriesId == seriesId)
|
.Where(s => s.SeriesId == seriesId)
|
||||||
.Include(s => s.ExternalReviews.Take(25))
|
.Include(s => s.ExternalReviews.Take(limit))
|
||||||
.Include(s => s.ExternalRatings.Take(25))
|
.Include(s => s.ExternalRatings.Take(limit))
|
||||||
.Include(s => s.ExternalRecommendations.Take(25))
|
.Include(s => s.ExternalRecommendations.Take(limit))
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
@ -138,7 +138,13 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
var seriesDetailPlusDto = new SeriesDetailPlusDto()
|
var seriesDetailPlusDto = new SeriesDetailPlusDto()
|
||||||
{
|
{
|
||||||
Ratings = seriesDetailDto.ExternalRatings.Select(r => _mapper.Map<RatingDto>(r)),
|
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()
|
Recommendations = new RecommendationDto()
|
||||||
{
|
{
|
||||||
ExternalSeries = externalSeriesRecommendations,
|
ExternalSeries = externalSeriesRecommendations,
|
||||||
|
@ -490,6 +490,10 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Files = new List<MangaFileDto>();
|
||||||
|
}
|
||||||
|
|
||||||
result.Chapters = await _context.Chapter
|
result.Chapters = await _context.Chapter
|
||||||
.Include(c => c.Files)
|
.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
|
// If there is 0 or 1 rating and that rating is you, return 0 back
|
||||||
var countOfRatingsThatAreUser = await _context.AppUserRating
|
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)
|
if (countOfRatingsThatAreUser == 1)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -12,8 +12,14 @@ public static class CronConverter
|
|||||||
"daily",
|
"daily",
|
||||||
"weekly",
|
"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
|
return source.ToLower() switch
|
||||||
{
|
{
|
||||||
"daily" => Cron.Daily(),
|
"daily" => Cron.Daily(),
|
||||||
|
@ -7,12 +7,8 @@ using System.Net;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs.Email;
|
using API.DTOs.Email;
|
||||||
using API.Entities.Enums;
|
|
||||||
using Flurl.Http;
|
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
|
||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
|
||||||
@ -38,7 +34,6 @@ public interface IEmailService
|
|||||||
Task<bool> SendForgotPasswordEmail(PasswordResetEmailDto dto);
|
Task<bool> SendForgotPasswordEmail(PasswordResetEmailDto dto);
|
||||||
Task<bool> SendFilesToEmail(SendToDto data);
|
Task<bool> SendFilesToEmail(SendToDto data);
|
||||||
Task<EmailTestResultDto> SendTestEmail(string adminEmail);
|
Task<EmailTestResultDto> SendTestEmail(string adminEmail);
|
||||||
Task<bool> IsDefaultEmailService();
|
|
||||||
Task SendEmailChangeEmail(ConfirmationEmailDto data);
|
Task SendEmailChangeEmail(ConfirmationEmailDto data);
|
||||||
bool IsValidEmail(string email);
|
bool IsValidEmail(string email);
|
||||||
}
|
}
|
||||||
@ -47,7 +42,6 @@ public class EmailService : IEmailService
|
|||||||
{
|
{
|
||||||
private readonly ILogger<EmailService> _logger;
|
private readonly ILogger<EmailService> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IDownloadService _downloadService;
|
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
|
|
||||||
private const string TemplatePath = @"{0}.html";
|
private const string TemplatePath = @"{0}.html";
|
||||||
@ -57,11 +51,10 @@ public class EmailService : IEmailService
|
|||||||
public const string DefaultApiUrl = "https://email.kavitareader.com";
|
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;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_downloadService = downloadService;
|
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,14 +107,6 @@ public class EmailService : IEmailService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Obsolete]
|
|
||||||
public async Task<bool> IsDefaultEmailService()
|
|
||||||
{
|
|
||||||
return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value!
|
|
||||||
.Equals(DefaultApiUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends an email that has a link that will finalize an Email Change
|
/// Sends an email that has a link that will finalize an Email Change
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -36,7 +36,7 @@ internal class ExternalMetadataIdsDto
|
|||||||
public MediaFormat? PlusMediaFormat { get; set; } = MediaFormat.Unknown;
|
public MediaFormat? PlusMediaFormat { get; set; } = MediaFormat.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SeriesDetailPlusAPIDto
|
internal class SeriesDetailPlusApiDto
|
||||||
{
|
{
|
||||||
public IEnumerable<MediaRecommendationDto> Recommendations { get; set; }
|
public IEnumerable<MediaRecommendationDto> Recommendations { get; set; }
|
||||||
public IEnumerable<UserReviewDto> Reviews { get; set; }
|
public IEnumerable<UserReviewDto> Reviews { get; set; }
|
||||||
@ -108,7 +108,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
if (!needsRefresh)
|
if (!needsRefresh)
|
||||||
{
|
{
|
||||||
// Convert into DTOs and return
|
// Convert into DTOs and return
|
||||||
return await SerializeExternalSeriesDetail(seriesId, externalSeriesMetadata, user, series);
|
return await SerializeExternalSeriesDetail(seriesId, series.LibraryId, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -123,7 +123,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
.WithHeader("Content-Type", "application/json")
|
.WithHeader("Content-Type", "application/json")
|
||||||
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
|
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
|
||||||
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
|
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
|
||||||
.ReceiveJson<SeriesDetailPlusAPIDto>();
|
.ReceiveJson<SeriesDetailPlusApiDto>();
|
||||||
|
|
||||||
|
|
||||||
// Clear out existing results
|
// Clear out existing results
|
||||||
@ -149,7 +149,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
// Recommendations
|
// Recommendations
|
||||||
|
|
||||||
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>();
|
externalSeriesMetadata.ExternalRecommendations ??= new List<ExternalRecommendation>();
|
||||||
var recs = await ProcessRecommendations(series, user!, result.Recommendations, externalSeriesMetadata);
|
var recs = await ProcessRecommendations(series, user, result.Recommendations, externalSeriesMetadata);
|
||||||
|
|
||||||
externalSeriesMetadata.LastUpdatedUtc = DateTime.UtcNow;
|
externalSeriesMetadata.LastUpdatedUtc = DateTime.UtcNow;
|
||||||
externalSeriesMetadata.AverageExternalRating = (int) externalSeriesMetadata.ExternalRatings
|
externalSeriesMetadata.AverageExternalRating = (int) externalSeriesMetadata.ExternalRatings
|
||||||
@ -161,14 +161,12 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
|
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
var ret = new SeriesDetailPlusDto()
|
return new SeriesDetailPlusDto()
|
||||||
{
|
{
|
||||||
Recommendations = recs,
|
Recommendations = recs,
|
||||||
Ratings = result.Ratings,
|
Ratings = result.Ratings,
|
||||||
Reviews = result.Reviews
|
Reviews = result.Reviews
|
||||||
};
|
};
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
catch (FlurlHttpException ex)
|
catch (FlurlHttpException ex)
|
||||||
{
|
{
|
||||||
@ -185,46 +183,9 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SeriesDetailPlusDto?> SerializeExternalSeriesDetail(int seriesId, ExternalSeriesMetadata externalSeriesMetadata,
|
private async Task<SeriesDetailPlusDto?> SerializeExternalSeriesDetail(int seriesId, int libraryId, AppUser user)
|
||||||
AppUser user, Series series)
|
|
||||||
{
|
{
|
||||||
var seriesIdsOnServer = externalSeriesMetadata.ExternalRecommendations
|
return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, libraryId, user);
|
||||||
.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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series)
|
private async Task<ExternalSeriesMetadata> GetExternalSeriesMetadataForSeries(int seriesId, Series series)
|
||||||
@ -249,8 +210,6 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
OwnedSeries = new List<SeriesDto>()
|
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
|
// NOTE: This can result in a series being recommended that shares the same name but different format
|
||||||
foreach (var rec in recs)
|
foreach (var rec in recs)
|
||||||
{
|
{
|
||||||
@ -276,7 +235,6 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canSeeExternalSeries) continue;
|
|
||||||
// We can show this based on user permissions
|
// We can show this based on user permissions
|
||||||
if (string.IsNullOrEmpty(rec.Name) || string.IsNullOrEmpty(rec.SiteUrl) || string.IsNullOrEmpty(rec.CoverUrl)) continue;
|
if (string.IsNullOrEmpty(rec.Name) || string.IsNullOrEmpty(rec.SiteUrl) || string.IsNullOrEmpty(rec.CoverUrl)) continue;
|
||||||
recDto.ExternalSeries.Add(new ExternalSeriesDto()
|
recDto.ExternalSeries.Add(new ExternalSeriesDto()
|
||||||
|
@ -32,7 +32,7 @@ namespace API.Services;
|
|||||||
public interface ISeriesService
|
public interface ISeriesService
|
||||||
{
|
{
|
||||||
Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId);
|
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> UpdateRating(AppUser user, UpdateSeriesRatingDto updateSeriesRatingDto);
|
||||||
Task<bool> DeleteMultipleSeries(IList<int> seriesIds);
|
Task<bool> DeleteMultipleSeries(IList<int> seriesIds);
|
||||||
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
|
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
|
||||||
@ -111,9 +111,8 @@ public class SeriesService : ISeriesService
|
|||||||
/// Updates the Series Metadata.
|
/// Updates the Series Metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="updateSeriesMetadataDto"></param>
|
/// <param name="updateSeriesMetadataDto"></param>
|
||||||
/// <param name="userId">If 0, does not bust any cache</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, int userId = 0)
|
public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
|
||||||
{
|
{
|
||||||
var hasWebLinksChanged = false;
|
var hasWebLinksChanged = false;
|
||||||
try
|
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");
|
_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");
|
_logger.LogDebug("Clearing cache as series weblinks may have changed");
|
||||||
await _cacheProvider.RemoveAsync(MetadataController.CacheKey + seriesId + userId);
|
await _cacheProvider.RemoveAsync(MetadataController.CacheKey + seriesId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -543,7 +543,7 @@ public static class Parser
|
|||||||
{
|
{
|
||||||
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
|
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-\d+(\.\d)?)?)",
|
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)(-c?\d+(\.\d)?)?)",
|
||||||
MatchOptions, RegexTimeout),
|
MatchOptions, RegexTimeout),
|
||||||
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
|
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
|
||||||
new Regex(
|
new Regex(
|
||||||
@ -761,6 +761,11 @@ public static class Parser
|
|||||||
var from = RemoveLeadingZeroes(tokens[0]);
|
var from = RemoveLeadingZeroes(tokens[0]);
|
||||||
if (tokens.Length != 2) return from;
|
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]);
|
var to = RemoveLeadingZeroes(hasPart ? AddChapterPart(tokens[1]) : tokens[1]);
|
||||||
return $"{from}-{to}";
|
return $"{from}-{to}";
|
||||||
}
|
}
|
||||||
|
@ -752,6 +752,8 @@ public class ProcessSeries : IProcessSeries
|
|||||||
.Where(s => !string.IsNullOrEmpty(s))
|
.Where(s => !string.IsNullOrEmpty(s))
|
||||||
.Select(s => s.Trim())
|
.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))
|
if (!string.IsNullOrEmpty(comicInfo.Isbn))
|
||||||
|
@ -48,6 +48,7 @@ public interface IVersionUpdaterService
|
|||||||
Task<UpdateNotificationDto?> CheckForUpdate();
|
Task<UpdateNotificationDto?> CheckForUpdate();
|
||||||
Task PushUpdate(UpdateNotificationDto update);
|
Task PushUpdate(UpdateNotificationDto update);
|
||||||
Task<IEnumerable<UpdateNotificationDto>> GetAllReleases();
|
Task<IEnumerable<UpdateNotificationDto>> GetAllReleases();
|
||||||
|
Task<int> GetNumberOfReleasesBehind();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VersionUpdaterService : IVersionUpdaterService
|
public class VersionUpdaterService : IVersionUpdaterService
|
||||||
@ -87,6 +88,12 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||||||
return updates.Select(CreateDto).Where(d => d != null)!;
|
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)
|
private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update)
|
||||||
{
|
{
|
||||||
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null;
|
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null;
|
||||||
|
@ -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"}
|
|
24
UI/Web/package-lock.json
generated
24
UI/Web/package-lock.json
generated
@ -681,6 +681,7 @@
|
|||||||
"version": "17.1.0",
|
"version": "17.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.1.0.tgz",
|
||||||
"integrity": "sha512-WDpO4WvC5ItjaRexnpFpKPpT+cu+5GYkWF8h74iHhfxOgU+gaQiMWERHylWCqF25AzmhKu0iI3ZZtaIJ6qqwog==",
|
"integrity": "sha512-WDpO4WvC5ItjaRexnpFpKPpT+cu+5GYkWF8h74iHhfxOgU+gaQiMWERHylWCqF25AzmhKu0iI3ZZtaIJ6qqwog==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "7.23.2",
|
"@babel/core": "7.23.2",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||||
@ -5674,6 +5675,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"picomatch": "^2.0.4"
|
"picomatch": "^2.0.4"
|
||||||
@ -5919,6 +5921,7 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -6224,6 +6227,7 @@
|
|||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -6472,7 +6476,8 @@
|
|||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
"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": {
|
"node_modules/cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
@ -7362,6 +7367,7 @@
|
|||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iconv-lite": "^0.6.2"
|
"iconv-lite": "^0.6.2"
|
||||||
@ -7371,6 +7377,7 @@
|
|||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
@ -8450,6 +8457,7 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -9224,6 +9232,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"binary-extensions": "^2.0.0"
|
"binary-extensions": "^2.0.0"
|
||||||
},
|
},
|
||||||
@ -11000,6 +11009,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -12390,6 +12400,7 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
},
|
},
|
||||||
@ -12400,7 +12411,8 @@
|
|||||||
"node_modules/reflect-metadata": {
|
"node_modules/reflect-metadata": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
"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": {
|
"node_modules/regenerate": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
@ -12852,7 +12864,7 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"devOptional": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.69.7",
|
"version": "1.69.7",
|
||||||
@ -12968,6 +12980,7 @@
|
|||||||
"version": "7.5.3",
|
"version": "7.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
|
||||||
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
|
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
},
|
},
|
||||||
@ -12982,6 +12995,7 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
},
|
},
|
||||||
@ -12992,7 +13006,8 @@
|
|||||||
"node_modules/semver/node_modules/yallist": {
|
"node_modules/semver/node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"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": {
|
"node_modules/send": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
@ -14099,6 +14114,7 @@
|
|||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||||
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
@ -63,7 +63,7 @@ export class ServerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bustCache() {
|
bustCache() {
|
||||||
return this.http.post(this.baseUrl + 'server/bust-review-and-rec-cache', {});
|
return this.http.post(this.baseUrl + 'server/bust-kavitaplus-cache', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMediaErrors() {
|
getMediaErrors() {
|
||||||
|
@ -45,6 +45,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required]));
|
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('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
|
||||||
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [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('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('port', new FormControl(this.serverSettings.port, [Validators.required]));
|
||||||
this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [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('cacheDirectory')?.setValue(this.serverSettings.cacheDirectory);
|
||||||
this.settingsForm.get('scanTask')?.setValue(this.serverSettings.taskScan);
|
this.settingsForm.get('scanTask')?.setValue(this.serverSettings.taskScan);
|
||||||
this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup);
|
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('ipAddresses')?.setValue(this.serverSettings.ipAddresses);
|
||||||
this.settingsForm.get('port')?.setValue(this.serverSettings.port);
|
this.settingsForm.get('port')?.setValue(this.serverSettings.port);
|
||||||
this.settingsForm.get('loggingLevel')?.setValue(this.serverSettings.loggingLevel);
|
this.settingsForm.get('loggingLevel')?.setValue(this.serverSettings.loggingLevel);
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
{{t('roles-title')}} <span *ngIf="roles.length === 0; else showRoles">{{null | defaultValue}}</span>
|
{{t('roles-title')}} <span *ngIf="roles.length === 0; else showRoles">{{null | defaultValue}}</span>
|
||||||
<ng-template #showRoles>
|
<ng-template #showRoles>
|
||||||
<ng-container *ngIf="hasAdminRole(member); else allRoles">
|
<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-container>
|
||||||
<ng-template #allRoles>
|
<ng-template #allRoles>
|
||||||
<app-tag-badge *ngFor="let role of roles" class="col-auto">{{role}}</app-tag-badge>
|
<app-tag-badge *ngFor="let role of roles" class="col-auto">{{role}}</app-tag-badge>
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
<app-carousel-reel [items]="data" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick(StreamId.OnDeck)">
|
<app-carousel-reel [items]="data" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick(StreamId.OnDeck)">
|
||||||
<ng-template #carouselItem let-item>
|
<ng-template #carouselItem let-item>
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [isOnDeck]="true"
|
<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>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
}
|
}
|
||||||
|
@ -193,12 +193,17 @@ export class DashboardComponent implements OnInit {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadStream(streamId: number) {
|
reloadStream(streamId: number, onDeck = false) {
|
||||||
const index = this.streams.findIndex(s => s.id === streamId);
|
const index = this.streams.findIndex(s => s.id === streamId);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
this.streams[index] = {...this.streams[index]};
|
if (onDeck) {
|
||||||
console.log('swapped out stream: ', this.streams[index]);
|
// TODO: Need to figure out a better way to refresh just one stream
|
||||||
this.cdRef.detectChanges();
|
this.refreshStreams$.next();
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
} else {
|
||||||
|
this.streams[index] = {...this.streams[index]};
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
|
async handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
|
||||||
|
@ -206,9 +206,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
|
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe((event) => this.handleScrollEvent(event));
|
.subscribe((event) => this.handleScrollEvent(event));
|
||||||
|
|
||||||
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend')
|
// fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend')
|
||||||
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
|
// .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe((event) => this.handleScrollEndEvent(event));
|
// .subscribe((event) => this.handleScrollEndEvent(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -558,7 +558,7 @@
|
|||||||
"invite-user": {
|
"invite-user": {
|
||||||
"title": "Invite User",
|
"title": "Invite User",
|
||||||
"close": "{{common.close}}",
|
"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}}",
|
"email": "{{common.email}}",
|
||||||
"required-field": "{{common.required-field}}",
|
"required-field": "{{common.required-field}}",
|
||||||
"setup-user-title": "User invited",
|
"setup-user-title": "User invited",
|
||||||
@ -1223,8 +1223,8 @@
|
|||||||
"title": "Recurring Tasks",
|
"title": "Recurring Tasks",
|
||||||
"library-scan-label": "Library Scan",
|
"library-scan-label": "Library Scan",
|
||||||
"library-scan-tooltip": "How often Kavita will scan and refresh metadata around library files.",
|
"library-scan-tooltip": "How often Kavita will scan and refresh metadata around library files.",
|
||||||
"library-database-backup-label": "Library Database Backup",
|
"library-database-backup-label": "Kavita Backup",
|
||||||
"library-database-backup-tooltip": "How often Kavita will backup the database.",
|
"library-database-backup-tooltip": "How often Kavita will backup the database and other related files.",
|
||||||
"cleanup-label": "Cleanup",
|
"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",
|
"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",
|
"adhoc-tasks-title": "Ad-hoc Tasks",
|
||||||
@ -1237,7 +1237,7 @@
|
|||||||
"required": "{{validation.required-field}}",
|
"required": "{{validation.required-field}}",
|
||||||
|
|
||||||
"custom-label": "Custom Schedule (Cron Notation)",
|
"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}}",
|
"recurring-tasks-title": "{{title}}",
|
||||||
"last-executed-header": "Last Executed",
|
"last-executed-header": "Last Executed",
|
||||||
@ -1302,6 +1302,7 @@
|
|||||||
"change-password-alt": "Change Password {{user}}",
|
"change-password-alt": "Change Password {{user}}",
|
||||||
"resend": "Resend",
|
"resend": "Resend",
|
||||||
"setup": "Setup",
|
"setup": "Setup",
|
||||||
|
"admin": "Admin",
|
||||||
"last-active-title": "Last Active:",
|
"last-active-title": "Last Active:",
|
||||||
"roles-title": "Roles:",
|
"roles-title": "Roles:",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
|
115
openapi.json
115
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.13.1"
|
"version": "0.7.13.2"
|
||||||
},
|
},
|
||||||
"servers": [
|
"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": {
|
"/api/Server/changelog": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -9649,7 +9682,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/Server/bust-review-and-rec-cache": {
|
"/api/Server/bust-kavitaplus-cache": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Server"
|
"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": {
|
"/api/Settings/is-email-setup": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -14782,24 +14765,6 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"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": {
|
"ExternalRating": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -19110,16 +19075,6 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"TestEmailDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"url": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"TokenRequestDto": {
|
"TokenRequestDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user