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 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));
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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}";
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
"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"
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
115
openapi.json
115
openapi.json
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user