Version Update Modal Rework + A few bugfixes (#3664)
This commit is contained in:
parent
9fb3bdd548
commit
43d0d1277f
65 changed files with 1963 additions and 805 deletions
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using System.Web;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs.Account;
|
||||
using API.Entities;
|
||||
using API.Errors;
|
||||
using Kavita.Common;
|
||||
|
@ -46,7 +47,7 @@ public class AccountService : IAccountService
|
|||
public async Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword)
|
||||
{
|
||||
var passwordValidationIssues = (await ValidatePassword(user, newPassword)).ToList();
|
||||
if (passwordValidationIssues.Any()) return passwordValidationIssues;
|
||||
if (passwordValidationIssues.Count != 0) return passwordValidationIssues;
|
||||
|
||||
var result = await _userManager.RemovePasswordAsync(user);
|
||||
if (!result.Succeeded)
|
||||
|
@ -55,15 +56,11 @@ public class AccountService : IAccountService
|
|||
return result.Errors.Select(e => new ApiException(400, e.Code, e.Description));
|
||||
}
|
||||
|
||||
|
||||
result = await _userManager.AddPasswordAsync(user, newPassword);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
_logger.LogError("Could not update password");
|
||||
return result.Errors.Select(e => new ApiException(400, e.Code, e.Description));
|
||||
}
|
||||
if (result.Succeeded) return [];
|
||||
|
||||
return new List<ApiException>();
|
||||
_logger.LogError("Could not update password");
|
||||
return result.Errors.Select(e => new ApiException(400, e.Code, e.Description));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ApiException>> ValidatePassword(AppUser user, string password)
|
||||
|
@ -81,15 +78,16 @@ public class AccountService : IAccountService
|
|||
}
|
||||
public async Task<IEnumerable<ApiException>> ValidateUsername(string username)
|
||||
{
|
||||
if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == username.ToUpper()))
|
||||
if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName != null
|
||||
&& x.NormalizedUserName.Equals(username, StringComparison.CurrentCultureIgnoreCase)))
|
||||
{
|
||||
return new List<ApiException>()
|
||||
{
|
||||
new ApiException(400, "Username is already taken")
|
||||
};
|
||||
return
|
||||
[
|
||||
new(400, "Username is already taken")
|
||||
];
|
||||
}
|
||||
|
||||
return Array.Empty<ApiException>();
|
||||
return [];
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ApiException>> ValidateEmail(string email)
|
||||
|
@ -112,6 +110,7 @@ public class AccountService : IAccountService
|
|||
{
|
||||
if (user == null) return false;
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
|
||||
return roles.Contains(PolicyConstants.BookmarkRole) || roles.Contains(PolicyConstants.AdminRole);
|
||||
}
|
||||
|
||||
|
@ -124,6 +123,7 @@ public class AccountService : IAccountService
|
|||
{
|
||||
if (user == null) return false;
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
|
||||
return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole);
|
||||
}
|
||||
|
||||
|
@ -135,9 +135,10 @@ public class AccountService : IAccountService
|
|||
public async Task<bool> CanChangeAgeRestriction(AppUser? user)
|
||||
{
|
||||
if (user == null) return false;
|
||||
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (roles.Contains(PolicyConstants.ReadOnlyRole)) return false;
|
||||
|
||||
return roles.Contains(PolicyConstants.ChangePasswordRole) || roles.Contains(PolicyConstants.AdminRole);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ public class CollectionTagService : ICollectionTagService
|
|||
if (!title.Equals(existingTag.Title) && await _unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, userId))
|
||||
throw new KavitaException("collection-tag-duplicate");
|
||||
|
||||
existingTag.Items ??= new List<Series>();
|
||||
existingTag.Items ??= [];
|
||||
if (existingTag.Source == ScrobbleProvider.Kavita)
|
||||
{
|
||||
existingTag.Title = title;
|
||||
|
@ -74,7 +74,7 @@ public class CollectionTagService : ICollectionTagService
|
|||
_unitOfWork.CollectionTagRepository.Update(existingTag);
|
||||
|
||||
// Check if Tag has updated (Summary)
|
||||
var summary = dto.Summary.Trim();
|
||||
var summary = (dto.Summary ?? string.Empty).Trim();
|
||||
if (existingTag.Summary == null || !existingTag.Summary.Equals(summary))
|
||||
{
|
||||
existingTag.Summary = summary;
|
||||
|
@ -105,7 +105,7 @@ public class CollectionTagService : ICollectionTagService
|
|||
{
|
||||
if (tag == null) return false;
|
||||
|
||||
tag.Items ??= new List<Series>();
|
||||
tag.Items ??= [];
|
||||
tag.Items = tag.Items.Where(s => !seriesIds.Contains(s.Id)).ToList();
|
||||
|
||||
if (tag.Items.Count == 0)
|
||||
|
|
|
@ -4,20 +4,14 @@ using System.Linq;
|
|||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace API.Services;
|
||||
#nullable enable
|
||||
|
||||
public class KavitaLocale
|
||||
{
|
||||
public string FileName { get; set; } // Key
|
||||
public string RenderName { get; set; }
|
||||
public float TranslationCompletion { get; set; }
|
||||
public bool IsRtL { get; set; }
|
||||
public string Hash { get; set; } // ETAG hash so I can run my own localization busting implementation
|
||||
}
|
||||
|
||||
|
||||
|
||||
public interface ILocalizationService
|
||||
|
|
|
@ -16,6 +16,7 @@ using API.DTOs.SeriesDetail;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Entities.MetadataMatching;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services.Tasks.Metadata;
|
||||
|
|
|
@ -278,7 +278,7 @@ public class LicenseService(
|
|||
var releases = await versionUpdaterService.GetAllReleases();
|
||||
response.IsValidVersion = releases
|
||||
.Where(r => !r.UpdateTitle.Contains("Hotfix")) // We don't care about Hotfix releases
|
||||
.Where(r => !r.IsPrerelease || BuildInfo.Version.IsWithinStableRelease(new Version(r.UpdateVersion))) // Ensure we don't take current nightlies within the current/last stable
|
||||
.Where(r => !r.IsPrerelease) // Ensure we don't take current nightlies within the current/last stable
|
||||
.Take(3)
|
||||
.All(r => new Version(r.UpdateVersion) <= BuildInfo.Version);
|
||||
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
public static class ReviewService
|
||||
{
|
||||
private const int BodyTextLimit = 175;
|
||||
public static IEnumerable<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
|
||||
{
|
||||
IList<UserReviewDto> externalReviews;
|
||||
var totalReviews = reviews.Count;
|
||||
|
||||
if (totalReviews > 10)
|
||||
{
|
||||
var stepSize = Math.Max((totalReviews - 4) / 8, 1);
|
||||
|
||||
var selectedReviews = new List<UserReviewDto>()
|
||||
{
|
||||
reviews[0],
|
||||
reviews[1],
|
||||
};
|
||||
for (var i = 2; i < totalReviews - 2; i += stepSize)
|
||||
{
|
||||
selectedReviews.Add(reviews[i]);
|
||||
|
||||
if (selectedReviews.Count >= 8)
|
||||
break;
|
||||
}
|
||||
|
||||
selectedReviews.Add(reviews[totalReviews - 2]);
|
||||
selectedReviews.Add(reviews[totalReviews - 1]);
|
||||
|
||||
externalReviews = selectedReviews;
|
||||
}
|
||||
else
|
||||
{
|
||||
externalReviews = reviews;
|
||||
}
|
||||
|
||||
return externalReviews.OrderByDescending(r => r.Score);
|
||||
}
|
||||
|
||||
public static string GetCharacters(string body)
|
||||
{
|
||||
if (string.IsNullOrEmpty(body)) return body;
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(body);
|
||||
|
||||
var textNodes = doc.DocumentNode.SelectNodes("//text()[not(parent::script)]");
|
||||
if (textNodes == null) return string.Empty;
|
||||
var plainText = string.Join(" ", textNodes
|
||||
.Select(node => node.InnerText)
|
||||
.Where(s => !s.Equals("\n")));
|
||||
|
||||
// Clean any leftover markdown out
|
||||
plainText = Regex.Replace(plainText, @"[_*\[\]~]", string.Empty);
|
||||
plainText = Regex.Replace(plainText, @"img\d*\((.*?)\)", string.Empty);
|
||||
plainText = Regex.Replace(plainText, @"~~~(.*?)~~~", "$1");
|
||||
plainText = Regex.Replace(plainText, @"\+{3}(.*?)\+{3}", "$1");
|
||||
plainText = Regex.Replace(plainText, @"~~(.*?)~~", "$1");
|
||||
plainText = Regex.Replace(plainText, @"__(.*?)__", "$1");
|
||||
plainText = Regex.Replace(plainText, @"#\s(.*?)", "$1");
|
||||
|
||||
// Just strip symbols
|
||||
plainText = Regex.Replace(plainText, @"[_*\[\]~]", string.Empty);
|
||||
plainText = Regex.Replace(plainText, @"img\d*\((.*?)\)", string.Empty);
|
||||
plainText = Regex.Replace(plainText, @"~~~", string.Empty);
|
||||
plainText = Regex.Replace(plainText, @"\+", string.Empty);
|
||||
plainText = Regex.Replace(plainText, @"~~", string.Empty);
|
||||
plainText = Regex.Replace(plainText, @"__", string.Empty);
|
||||
|
||||
// Take the first BodyTextLimit characters
|
||||
plainText = plainText.Length > BodyTextLimit ? plainText.Substring(0, BodyTextLimit) : plainText;
|
||||
|
||||
return plainText + "…";
|
||||
}
|
||||
|
||||
}
|
441
API/Services/SettingsService.cs
Normal file
441
API/Services/SettingsService.cs
Normal file
|
@ -0,0 +1,441 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.KavitaPlus.Metadata;
|
||||
using API.DTOs.Settings;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Logging;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Kavita.Common.Helpers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<ActionResult<MetadataSettingsDto>> UpdateMetadataSettings(MetadataSettingsDto dto);
|
||||
Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto);
|
||||
}
|
||||
|
||||
|
||||
public class SettingsService : ISettingsService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly ILogger<SettingsService> _logger;
|
||||
|
||||
public SettingsService(IUnitOfWork unitOfWork, IDirectoryService directoryService,
|
||||
ILibraryWatcher libraryWatcher, ITaskScheduler taskScheduler,
|
||||
ILogger<SettingsService> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
_taskScheduler = taskScheduler;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the metadata settings for Kavita+ Metadata feature
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<ActionResult<MetadataSettingsDto>> UpdateMetadataSettings(MetadataSettingsDto dto)
|
||||
{
|
||||
var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings();
|
||||
existingMetadataSetting.Enabled = dto.Enabled;
|
||||
existingMetadataSetting.EnableSummary = dto.EnableSummary;
|
||||
existingMetadataSetting.EnableLocalizedName = dto.EnableLocalizedName;
|
||||
existingMetadataSetting.EnablePublicationStatus = dto.EnablePublicationStatus;
|
||||
existingMetadataSetting.EnableRelationships = dto.EnableRelationships;
|
||||
existingMetadataSetting.EnablePeople = dto.EnablePeople;
|
||||
existingMetadataSetting.EnableStartDate = dto.EnableStartDate;
|
||||
existingMetadataSetting.EnableGenres = dto.EnableGenres;
|
||||
existingMetadataSetting.EnableTags = dto.EnableTags;
|
||||
existingMetadataSetting.FirstLastPeopleNaming = dto.FirstLastPeopleNaming;
|
||||
existingMetadataSetting.EnableCoverImage = dto.EnableCoverImage;
|
||||
|
||||
existingMetadataSetting.AgeRatingMappings = dto.AgeRatingMappings ?? [];
|
||||
|
||||
existingMetadataSetting.Blacklist = (dto.Blacklist ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? [];
|
||||
existingMetadataSetting.Whitelist = (dto.Whitelist ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? [];
|
||||
existingMetadataSetting.Overrides = [.. dto.Overrides ?? []];
|
||||
existingMetadataSetting.PersonRoles = dto.PersonRoles ?? [];
|
||||
|
||||
// Handle Field Mappings
|
||||
|
||||
// Clear existing mappings
|
||||
existingMetadataSetting.FieldMappings ??= [];
|
||||
_unitOfWork.SettingsRepository.RemoveRange(existingMetadataSetting.FieldMappings);
|
||||
existingMetadataSetting.FieldMappings.Clear();
|
||||
|
||||
if (dto.FieldMappings != null)
|
||||
{
|
||||
// Add new mappings
|
||||
foreach (var mappingDto in dto.FieldMappings)
|
||||
{
|
||||
existingMetadataSetting.FieldMappings.Add(new MetadataFieldMapping
|
||||
{
|
||||
SourceType = mappingDto.SourceType,
|
||||
DestinationType = mappingDto.DestinationType,
|
||||
SourceValue = mappingDto.SourceValue,
|
||||
DestinationValue = mappingDto.DestinationValue,
|
||||
ExcludeFromSource = mappingDto.ExcludeFromSource
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save changes
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Return updated settings
|
||||
return await _unitOfWork.SettingsRepository.GetMetadataSettingDto();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update Server Settings
|
||||
/// </summary>
|
||||
/// <param name="updateSettingsDto"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="KavitaException"></exception>
|
||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
var updateBookmarks = false;
|
||||
var originalBookmarkDirectory = _directoryService.BookmarkDirectory;
|
||||
|
||||
var bookmarkDirectory = updateSettingsDto.BookmarksDirectory;
|
||||
if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") &&
|
||||
!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/"))
|
||||
{
|
||||
bookmarkDirectory =
|
||||
_directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory))
|
||||
{
|
||||
bookmarkDirectory = _directoryService.BookmarkDirectory;
|
||||
}
|
||||
|
||||
var updateTask = false;
|
||||
foreach (var setting in currentSettings)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.OnDeckProgressDays &&
|
||||
updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.OnDeckUpdateDays &&
|
||||
updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
|
||||
{
|
||||
if (OsInfo.IsDocker) continue;
|
||||
setting.Value = updateSettingsDto.Port + string.Empty;
|
||||
// Port is managed in appSetting.json
|
||||
Configuration.Port = updateSettingsDto.Port;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.CacheSize &&
|
||||
updateSettingsDto.CacheSize + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.CacheSize + string.Empty;
|
||||
// CacheSize is managed in appSetting.json
|
||||
Configuration.CacheSize = updateSettingsDto.CacheSize;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
updateTask = updateTask || UpdateSchedulingSettings(setting, updateSettingsDto);
|
||||
|
||||
UpdateEmailSettings(setting, updateSettingsDto);
|
||||
|
||||
|
||||
|
||||
if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value)
|
||||
{
|
||||
if (OsInfo.IsDocker) continue;
|
||||
// Validate IP addresses
|
||||
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',',
|
||||
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!IPAddress.TryParse(ipAddress.Trim(), out _))
|
||||
{
|
||||
throw new KavitaException("ip-address-invalid");
|
||||
}
|
||||
}
|
||||
|
||||
setting.Value = updateSettingsDto.IpAddresses;
|
||||
// IpAddresses is managed in appSetting.json
|
||||
Configuration.IpAddresses = updateSettingsDto.IpAddresses;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
|
||||
{
|
||||
var path = !updateSettingsDto.BaseUrl.StartsWith('/')
|
||||
? $"/{updateSettingsDto.BaseUrl}"
|
||||
: updateSettingsDto.BaseUrl;
|
||||
path = !path.EndsWith('/')
|
||||
? $"{path}/"
|
||||
: path;
|
||||
setting.Value = path;
|
||||
Configuration.BaseUrl = updateSettingsDto.BaseUrl;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.LoggingLevel &&
|
||||
updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
||||
LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableOpds &&
|
||||
updateSettingsDto.EnableOpds + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableOpds + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EncodeMediaAs &&
|
||||
((int)updateSettingsDto.EncodeMediaAs).ToString() != setting.Value)
|
||||
{
|
||||
setting.Value = ((int)updateSettingsDto.EncodeMediaAs).ToString();
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.CoverImageSize &&
|
||||
((int)updateSettingsDto.CoverImageSize).ToString() != setting.Value)
|
||||
{
|
||||
setting.Value = ((int)updateSettingsDto.CoverImageSize).ToString();
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = (updateSettingsDto.HostName + string.Empty).Trim();
|
||||
setting.Value = UrlHelper.RemoveEndingSlash(setting.Value);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
||||
{
|
||||
// Validate new directory can be used
|
||||
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
|
||||
{
|
||||
throw new KavitaException("bookmark-dir-permissions");
|
||||
}
|
||||
|
||||
originalBookmarkDirectory = setting.Value;
|
||||
|
||||
// Normalize the path deliminators. Just to look nice in DB, no functionality
|
||||
setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
updateBookmarks = true;
|
||||
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.AllowStatCollection &&
|
||||
updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TotalBackups &&
|
||||
updateSettingsDto.TotalBackups + string.Empty != setting.Value)
|
||||
{
|
||||
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
||||
{
|
||||
throw new KavitaException("total-backups");
|
||||
}
|
||||
|
||||
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TotalLogs &&
|
||||
updateSettingsDto.TotalLogs + string.Empty != setting.Value)
|
||||
{
|
||||
if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1)
|
||||
{
|
||||
throw new KavitaException("total-logs");
|
||||
}
|
||||
|
||||
setting.Value = updateSettingsDto.TotalLogs + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableFolderWatching &&
|
||||
updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return updateSettingsDto;
|
||||
|
||||
try
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (!updateSettingsDto.AllowStatCollection)
|
||||
{
|
||||
_taskScheduler.CancelStatsTasks();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _taskScheduler.ScheduleStatsTasks();
|
||||
}
|
||||
|
||||
if (updateBookmarks)
|
||||
{
|
||||
UpdateBookmarkDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
||||
}
|
||||
|
||||
if (updateTask)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _taskScheduler.ScheduleTasks());
|
||||
}
|
||||
|
||||
if (updateSettingsDto.EnableFolderWatching)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _libraryWatcher.StartWatching());
|
||||
}
|
||||
else
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _libraryWatcher.StopWatching());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when updating server settings");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw new KavitaException("generic-error");
|
||||
}
|
||||
|
||||
|
||||
_logger.LogInformation("Server Settings updated");
|
||||
|
||||
return updateSettingsDto;
|
||||
}
|
||||
|
||||
private void UpdateBookmarkDirectory(string originalBookmarkDirectory, string bookmarkDirectory)
|
||||
{
|
||||
_directoryService.ExistOrCreate(bookmarkDirectory);
|
||||
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
||||
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
|
||||
}
|
||||
|
||||
private bool UpdateSchedulingSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskBackup;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskScan;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TaskCleanup && updateSettingsDto.TaskCleanup != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskCleanup;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.EmailHost &&
|
||||
updateSettingsDto.SmtpConfig.Host + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.Host + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailPort &&
|
||||
updateSettingsDto.SmtpConfig.Port + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.Port + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailAuthPassword &&
|
||||
updateSettingsDto.SmtpConfig.Password + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.Password + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailAuthUserName &&
|
||||
updateSettingsDto.SmtpConfig.UserName + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.UserName + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailSenderAddress &&
|
||||
updateSettingsDto.SmtpConfig.SenderAddress + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.SenderAddress + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailSenderDisplayName &&
|
||||
updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailSizeLimit &&
|
||||
updateSettingsDto.SmtpConfig.SizeLimit + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.SizeLimit + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailEnableSsl &&
|
||||
updateSettingsDto.SmtpConfig.EnableSsl + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.EnableSsl + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailCustomizedTemplates &&
|
||||
updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,7 +36,6 @@ public interface IStatisticService
|
|||
IEnumerable<StatCount<int>> GetWordsReadCountByYear(int userId = 0);
|
||||
Task UpdateServerStatistics();
|
||||
Task<long> TimeSpentReadingForUsersAsync(IList<int> userIds, IList<int> libraryIds);
|
||||
Task<KavitaPlusMetadataBreakdownDto> GetKavitaPlusMetadataBreakdown();
|
||||
Task<IEnumerable<FileExtensionExportDto>> GetFilesByExtension(string fileExtension);
|
||||
}
|
||||
|
||||
|
@ -139,7 +138,9 @@ public class StatisticService : IStatisticService
|
|||
}
|
||||
else
|
||||
{
|
||||
#pragma warning disable S6561
|
||||
var timeDifference = DateTime.Now - earliestReadDate;
|
||||
#pragma warning restore S6561
|
||||
var deltaWeeks = (int)Math.Ceiling(timeDifference.TotalDays / 7);
|
||||
|
||||
averageReadingTimePerWeek /= deltaWeeks;
|
||||
|
@ -554,29 +555,6 @@ public class StatisticService : IStatisticService
|
|||
p.chapter.AvgHoursToRead * (p.progress.PagesRead / (1.0f * p.chapter.Pages))));
|
||||
}
|
||||
|
||||
public async Task<KavitaPlusMetadataBreakdownDto> GetKavitaPlusMetadataBreakdown()
|
||||
{
|
||||
// We need to count number of Series that have an external series record
|
||||
// Then count how many series are blacklisted
|
||||
// Then get total count of series that are Kavita+ eligible
|
||||
var plusLibraries = await _context.Library
|
||||
.Where(l => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(l.Type))
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var countOfBlacklisted = await _context.SeriesBlacklist.CountAsync();
|
||||
var totalSeries = await _context.Series.Where(s => plusLibraries.Contains(s.LibraryId)).CountAsync();
|
||||
var seriesWithMetadata = await _context.ExternalSeriesMetadata.CountAsync();
|
||||
|
||||
return new KavitaPlusMetadataBreakdownDto()
|
||||
{
|
||||
TotalSeries = totalSeries,
|
||||
ErroredSeries = countOfBlacklisted,
|
||||
SeriesCompleted = seriesWithMetadata
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<FileExtensionExportDto>> GetFilesByExtension(string fileExtension)
|
||||
{
|
||||
var query = _context.MangaFile
|
||||
|
|
|
@ -7,6 +7,7 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Converters;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks;
|
||||
|
|
|
@ -86,7 +86,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag
|
|||
{
|
||||
ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret);
|
||||
}
|
||||
|
||||
ret.Title = Parser.CleanSpecialTitle(fileName);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ret.Series))
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
@ -44,87 +43,83 @@ public static partial class Parser
|
|||
"One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel",
|
||||
"GN", "FCBD", "Giant Size");
|
||||
|
||||
private static readonly char[] LeadingZeroesTrimChars = new[] { '0' };
|
||||
private static readonly char[] LeadingZeroesTrimChars = ['0'];
|
||||
|
||||
private static readonly char[] SpacesAndSeparators = { '\0', '\t', '\r', ' ', '-', ','};
|
||||
private static readonly char[] SpacesAndSeparators = ['\0', '\t', '\r', ' ', '-', ','];
|
||||
|
||||
|
||||
private const string Number = @"\d+(\.\d)?";
|
||||
private const string NumberRange = Number + @"(-" + Number + @")?";
|
||||
|
||||
/// <summary>
|
||||
/// non greedy matching of a string where parenthesis are balanced
|
||||
/// non-greedy matching of a string where parenthesis are balanced
|
||||
/// </summary>
|
||||
public const string BalancedParen = @"(?:[^()]|(?<open>\()|(?<-open>\)))*?(?(open)(?!))";
|
||||
/// <summary>
|
||||
/// non greedy matching of a string where square brackets are balanced
|
||||
/// non-greedy matching of a string where square brackets are balanced
|
||||
/// </summary>
|
||||
public const string BalancedBracket = @"(?:[^\[\]]|(?<open>\[)|(?<-open>\]))*?(?(open)(?!))";
|
||||
/// <summary>
|
||||
/// Matches [Complete], release tags like [kmts] but not [ Complete ] or [kmts ]
|
||||
/// </summary>
|
||||
private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(?<!\s)\]";
|
||||
/// <summary>
|
||||
/// Common regex patterns present in both Comics and Mangas
|
||||
/// </summary>
|
||||
private const string CommonSpecial = @"Specials?|One[- ]?Shot|Extra(?:\sChapter)?(?=\s)|Art Collection|Side Stories|Bonus";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data
|
||||
/// </summary>
|
||||
/// <remarks>See here for some examples https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face</remarks>
|
||||
public static readonly Regex FontSrcUrlRegex = new Regex(@"(?<Start>(?:src:\s?)?(?:url|local)\((?!data:)" + "(?:[\"']?)" + @"(?!data:))"
|
||||
+ "(?<Filename>(?!data:)[^\"']+?)" + "(?<End>[\"']?" + @"\);?)",
|
||||
public static readonly Regex FontSrcUrlRegex = new(@"(?<Start>(?:src:\s?)?(?:url|local)\((?!data:)" + "(?:[\"']?)" + @"(?!data:))"
|
||||
+ "(?<Filename>(?!data:)[^\"']+?)" + "(?<End>[\"']?" + @"\);?)",
|
||||
MatchOptions, RegexTimeout);
|
||||
/// <summary>
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/@import
|
||||
/// </summary>
|
||||
public static readonly Regex CssImportUrlRegex = new Regex("(@import\\s([\"|']|url\\([\"|']))(?<Filename>[^'\"]+)([\"|']\\)?);",
|
||||
public static readonly Regex CssImportUrlRegex = new("(@import\\s([\"|']|url\\([\"|']))(?<Filename>[^'\"]+)([\"|']\\)?);",
|
||||
MatchOptions | RegexOptions.Multiline, RegexTimeout);
|
||||
/// <summary>
|
||||
/// Misc css image references, like background-image: url(), border-image, or list-style-image
|
||||
/// </summary>
|
||||
/// Original prepend: (background|border|list-style)-image:\s?)?
|
||||
public static readonly Regex CssImageUrlRegex = new Regex(@"(url\((?!data:).(?!data:))" + "(?<Filename>(?!data:)[^\"']*)" + @"(.\))",
|
||||
public static readonly Regex CssImageUrlRegex = new(@"(url\((?!data:).(?!data:))" + "(?<Filename>(?!data:)[^\"']*)" + @"(.\))",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
|
||||
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions,
|
||||
private static readonly Regex ImageRegex = new(ImageFileExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions,
|
||||
private static readonly Regex ArchiveFileRegex = new(ArchiveFileExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex ComicInfoArchiveRegex = new Regex(@"\.cbz|\.cbr|\.cb7|\.cbt",
|
||||
private static readonly Regex ComicInfoArchiveRegex = new(@"\.cbz|\.cbr|\.cb7|\.cbt",
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions,
|
||||
private static readonly Regex XmlRegex = new(XmlRegexExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
||||
private static readonly Regex BookFileRegex = new(BookFileExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(?<!back)(?<!back_)(?<!back-)(cover|folder)(?![\w\d])",
|
||||
private static readonly Regex CoverImageRegex = new(@"(?<!back[\s_-])(?<!\(back )(?<!back)(?:^|[^a-zA-Z0-9])(!?cover|folder)(?![a-zA-Z0-9]|s\b)",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Normalize everything within Kavita. Some characters don't fall under Unicode, like full-width characters and need to be
|
||||
/// added on a case-by-case basis.
|
||||
/// </summary>
|
||||
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+!*!+]",
|
||||
private static readonly Regex NormalizeRegex = new(@"[^\p{L}0-9\+!*!+]",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Supports Batman (2020) or Batman (2)
|
||||
/// </summary>
|
||||
private static readonly Regex SeriesAndYearRegex = new Regex(@"^\D+\s\((?<Year>\d+)\)$",
|
||||
private static readonly Regex SeriesAndYearRegex = new(@"^\D+\s\((?<Year>\d+)\)$",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Recognizes the Special token only
|
||||
/// </summary>
|
||||
private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+",
|
||||
private static readonly Regex SpecialTokenRegex = new(@"SP\d+",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
|
||||
private static readonly Regex[] MangaVolumeRegex = new[]
|
||||
{
|
||||
private static readonly Regex[] MangaVolumeRegex =
|
||||
[
|
||||
// Thai Volume: เล่ม n -> Volume n
|
||||
new Regex(
|
||||
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
|
||||
|
@ -197,11 +192,11 @@ public static partial class Parser
|
|||
// Russian Volume: n Том -> Volume n
|
||||
new Regex(
|
||||
@"(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
MatchOptions, RegexTimeout)
|
||||
];
|
||||
|
||||
private static readonly Regex[] MangaSeriesRegex = new[]
|
||||
{
|
||||
private static readonly Regex[] MangaSeriesRegex =
|
||||
[
|
||||
// Thai Volume: เล่ม n -> Volume n
|
||||
new Regex(
|
||||
@"(?<Series>.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
|
||||
|
@ -374,12 +369,12 @@ public static partial class Parser
|
|||
// Japanese Volume: n巻 -> Volume n
|
||||
new Regex(
|
||||
@"(?<Series>.+?)第(?<Volume>\d+(?:(\-)\d+)?)巻",
|
||||
MatchOptions, RegexTimeout),
|
||||
MatchOptions, RegexTimeout)
|
||||
|
||||
};
|
||||
];
|
||||
|
||||
private static readonly Regex[] ComicSeriesRegex = new[]
|
||||
{
|
||||
private static readonly Regex[] ComicSeriesRegex =
|
||||
[
|
||||
// Thai Volume: เล่ม n -> Volume n
|
||||
new Regex(
|
||||
@"(?<Series>.+?)(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
|
||||
|
@ -467,11 +462,11 @@ public static partial class Parser
|
|||
// MUST BE LAST: Batman & Daredevil - King of New York
|
||||
new Regex(
|
||||
@"^(?<Series>.*)",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
MatchOptions, RegexTimeout)
|
||||
];
|
||||
|
||||
private static readonly Regex[] ComicVolumeRegex = new[]
|
||||
{
|
||||
private static readonly Regex[] ComicVolumeRegex =
|
||||
[
|
||||
// Thai Volume: เล่ม n -> Volume n
|
||||
new Regex(
|
||||
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
|
||||
|
@ -507,11 +502,11 @@ public static partial class Parser
|
|||
// Russian Volume: n Том -> Volume n
|
||||
new Regex(
|
||||
@"(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
MatchOptions, RegexTimeout)
|
||||
];
|
||||
|
||||
private static readonly Regex[] ComicChapterRegex = new[]
|
||||
{
|
||||
private static readonly Regex[] ComicChapterRegex =
|
||||
[
|
||||
// Thai Volume: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n
|
||||
new Regex(
|
||||
@"(บทที่|ตอนที่)(\s)?(\.?)(\s|_)?(?<Chapter>\d+(\-\d+)?(\.\d+)?)",
|
||||
|
@ -576,11 +571,11 @@ public static partial class Parser
|
|||
// spawn-123, spawn-chapter-123 (from https://github.com/Girbons/comics-downloader)
|
||||
new Regex(
|
||||
@"^(?<Series>.+?)-(chapter-)?(?<Chapter>\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
MatchOptions, RegexTimeout)
|
||||
];
|
||||
|
||||
private static readonly Regex[] MangaChapterRegex = new[]
|
||||
{
|
||||
private static readonly Regex[] MangaChapterRegex =
|
||||
[
|
||||
// Thai Chapter: บทที่ n -> Chapter n, ตอนที่ n -> Chapter n, เล่ม n -> Volume n, เล่มที่ n -> Volume n
|
||||
new Regex(
|
||||
@"(?<Volume>((เล่ม|เล่มที่))?(\s|_)?\.?\d+)(\s|_)(บทที่|ตอนที่)\.?(\s|_)?(?<Chapter>\d+)",
|
||||
|
@ -645,8 +640,8 @@ public static partial class Parser
|
|||
// Russian Chapter: n Главa -> Chapter n
|
||||
new Regex(
|
||||
@"(?!Том)(?<!Том\.)\s\d+(\s|_)?(?<Chapter>\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
MatchOptions, RegexTimeout)
|
||||
];
|
||||
|
||||
private static readonly Regex MangaEditionRegex = new Regex(
|
||||
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
||||
|
@ -661,25 +656,6 @@ public static partial class Parser
|
|||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
private static readonly Regex MangaSpecialRegex = new Regex(
|
||||
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
|
||||
$@"\b(?:{CommonSpecial}|Omake)\b",
|
||||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
private static readonly Regex ComicSpecialRegex = new Regex(
|
||||
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
|
||||
$@"\b(?:{CommonSpecial}|\d.+?(\W|-|^)Annual|Annual(\W|-|$|\s#)|Book \d.+?|Compendium(\W|-|$|\s.+?)|Omnibus(\W|-|$|\s.+?)|FCBD \d.+?|Absolute(\W|-|$|\s.+?)|Preview(\W|-|$|\s.+?)|Hors[ -]S[ée]rie|TPB|HS|THS)\b",
|
||||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
private static readonly Regex EuropeanComicRegex = new Regex(
|
||||
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
|
||||
@"\b(?:Bd[-\s]Fr)\b",
|
||||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
|
||||
// If SP\d+ is in the filename, we force treat it as a special regardless if volume or chapter might have been found.
|
||||
private static readonly Regex SpecialMarkerRegex = new Regex(
|
||||
@"SP\d+",
|
||||
|
@ -732,20 +708,6 @@ public static partial class Parser
|
|||
return HasSpecialMarker(filePath);
|
||||
}
|
||||
|
||||
private static bool IsMangaSpecial(string? filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)) return false;
|
||||
return HasSpecialMarker(filePath);
|
||||
}
|
||||
|
||||
private static bool IsComicSpecial(string? filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)) return false;
|
||||
return HasSpecialMarker(filePath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static string ParseMangaSeries(string filename)
|
||||
{
|
||||
foreach (var regex in MangaSeriesRegex)
|
||||
|
@ -932,22 +894,6 @@ public static partial class Parser
|
|||
return title;
|
||||
}
|
||||
|
||||
private static string RemoveMangaSpecialTags(string title)
|
||||
{
|
||||
return MangaSpecialRegex.Replace(title, string.Empty);
|
||||
}
|
||||
|
||||
private static string RemoveEuropeanTags(string title)
|
||||
{
|
||||
return EuropeanComicRegex.Replace(title, string.Empty);
|
||||
}
|
||||
|
||||
private static string RemoveComicSpecialTags(string title)
|
||||
{
|
||||
return ComicSpecialRegex.Replace(title, string.Empty);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Translates _ -> spaces, trims front and back of string, removes release groups
|
||||
|
@ -966,20 +912,6 @@ public static partial class Parser
|
|||
|
||||
title = RemoveEditionTagHolders(title);
|
||||
|
||||
// if (replaceSpecials)
|
||||
// {
|
||||
// if (isComic)
|
||||
// {
|
||||
// title = RemoveComicSpecialTags(title);
|
||||
// title = RemoveEuropeanTags(title);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// title = RemoveMangaSpecialTags(title);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
title = title.Trim(SpacesAndSeparators);
|
||||
|
||||
title = EmptySpaceRegex.Replace(title, " ");
|
||||
|
@ -1110,11 +1042,6 @@ public static partial class Parser
|
|||
{
|
||||
if (string.IsNullOrEmpty(name)) return name;
|
||||
var cleaned = SpecialTokenRegex.Replace(name.Replace('_', ' '), string.Empty).Trim();
|
||||
var lastIndex = cleaned.LastIndexOf('.');
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
cleaned = cleaned.Substring(0, cleaned.LastIndexOf('.')).Trim();
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(cleaned) ? name : cleaned;
|
||||
}
|
||||
|
@ -1132,7 +1059,7 @@ public static partial class Parser
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a Path doesn't start with certain blacklisted folders, like __MACOSX, @Recently-Snapshot, etc and that if a full path, the filename
|
||||
/// Validates that a Path doesn't start with certain blacklisted folders, like __MACOSX, @Recently-Snapshot, etc. and that if a full path, the filename
|
||||
/// doesn't start with ._, which is a metadata file on MACOSX.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
|
|
|
@ -51,7 +51,7 @@ public interface IVersionUpdaterService
|
|||
Task<UpdateNotificationDto?> CheckForUpdate();
|
||||
Task PushUpdate(UpdateNotificationDto update);
|
||||
Task<IList<UpdateNotificationDto>> GetAllReleases(int count = 0);
|
||||
Task<int> GetNumberOfReleasesBehind();
|
||||
Task<int> GetNumberOfReleasesBehind(bool stableOnly = false);
|
||||
}
|
||||
|
||||
|
||||
|
@ -112,6 +112,10 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will add any extra (nightly) updates from the latest stable. Does not back-fill anything prior to the latest stable.
|
||||
/// </summary>
|
||||
/// <param name="dtos"></param>
|
||||
private async Task EnrichWithNightlyInfo(List<UpdateNotificationDto> dtos)
|
||||
{
|
||||
var dto = dtos[0]; // Latest version
|
||||
|
@ -301,7 +305,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
}
|
||||
|
||||
// If we're on a nightly build, enrich the information
|
||||
if (updateDtos.Count != 0 && BuildInfo.Version > new Version(updateDtos[0].UpdateVersion))
|
||||
if (updateDtos.Count != 0) // && BuildInfo.Version > new Version(updateDtos[0].UpdateVersion)
|
||||
{
|
||||
await EnrichWithNightlyInfo(updateDtos);
|
||||
}
|
||||
|
@ -397,22 +401,25 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
}
|
||||
|
||||
|
||||
public async Task<int> GetNumberOfReleasesBehind()
|
||||
/// <summary>
|
||||
/// Returns the number of releases ahead of this install version. If this install version is on a nightly,
|
||||
/// then include nightly releases, otherwise only count Stable releases.
|
||||
/// </summary>
|
||||
/// <param name="stableOnly">Only count Stable releases </param>
|
||||
/// <returns></returns>
|
||||
public async Task<int> GetNumberOfReleasesBehind(bool stableOnly = false)
|
||||
{
|
||||
var updates = await GetAllReleases();
|
||||
|
||||
// If the user is on nightly, then we need to handle releases behind differently
|
||||
if (updates[0].IsPrerelease)
|
||||
if (!stableOnly && (updates[0].IsPrerelease || updates[0].IsOnNightlyInRelease))
|
||||
{
|
||||
return Math.Min(0, updates
|
||||
.TakeWhile(update => update.UpdateVersion != update.CurrentVersion)
|
||||
.Count() - 1);
|
||||
return updates.Count(u => u.IsReleaseNewer);
|
||||
}
|
||||
|
||||
return Math.Min(0, updates
|
||||
return updates
|
||||
.Where(update => !update.IsPrerelease)
|
||||
.TakeWhile(update => update.UpdateVersion != update.CurrentVersion)
|
||||
.Count());
|
||||
.Count(u => u.IsReleaseNewer);
|
||||
}
|
||||
|
||||
private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue