diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/API.Tests/Extensions/SeriesExtensionsTests.cs index eacd49ae9..6a706e892 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/API.Tests/Extensions/SeriesExtensionsTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.Linq; using API.Comparators; using API.Entities; @@ -31,7 +32,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Special 1", series.GetCoverImage()); @@ -66,7 +67,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage()); @@ -108,7 +109,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage()); @@ -134,7 +135,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Special 2", series.GetCoverImage()); @@ -164,7 +165,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Chapter 2", series.GetCoverImage()); @@ -201,7 +202,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1", series.GetCoverImage()); @@ -238,7 +239,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1", series.GetCoverImage()); @@ -282,7 +283,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Volume 1", series.GetCoverImage()); @@ -315,7 +316,7 @@ public class SeriesExtensionsTests foreach (var vol in series.Volumes) { - vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; } Assert.Equal("Chapter 2", series.GetCoverImage()); diff --git a/API.Tests/Helpers/SmartFilterHelperTests.cs b/API.Tests/Helpers/SmartFilterHelperTests.cs index 3d9fbc3ca..510748821 100644 --- a/API.Tests/Helpers/SmartFilterHelperTests.cs +++ b/API.Tests/Helpers/SmartFilterHelperTests.cs @@ -1,8 +1,11 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.Entities.Enums; using API.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Xunit; namespace API.Tests.Helpers; @@ -13,21 +16,47 @@ public class SmartFilterHelperTests public void Test_Decode() { var encoded = """ - stmts=comparison%3D5%26field%3D18%26value%3D6%2Ccomparison%3D0%26field%3D4%26value%3D0%2Ccomparison%3D7%26field%3D1%26value%3Da&sortOptions=sortField=1&isAscending=true&limitTo=0&combination=1 + stmts=comparison%3D5%26field%3D18%26value%3D95%2Ccomparison%3D0%26field%3D4%26value%3D0%2Ccomparison%3D7%26field%3D1%26value%3Da&sortOptions=sortField=2&isAscending=false&limitTo=10&combination=1 """; var filter = SmartFilterHelper.Decode(encoded); - Assert.Equal(0, filter.LimitTo); - Assert.Equal(SortField.SortName, filter.SortOptions.SortField); - Assert.True(filter.SortOptions.IsAscending); + Assert.Equal(10, filter.LimitTo); + Assert.Equal(SortField.CreatedDate, filter.SortOptions.SortField); + Assert.False(filter.SortOptions.IsAscending); Assert.Null(filter.Name); var list = filter.Statements.ToList(); AssertStatementSame(list[2], FilterField.SeriesName, FilterComparison.Matches, "a"); - AssertStatementSame(list[1], FilterField.AgeRating, FilterComparison.Equal, (int) AgeRating.Unknown + ""); - AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Contains, "6"); + AssertStatementSame(list[1], FilterField.AgeRating, FilterComparison.Equal, (int) AgeRating.Unknown + string.Empty); + AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Contains, "95"); + } + [Fact] + public void Test_Encode() + { + var filter = new FilterV2Dto() + { + Name = "Test", + SortOptions = new SortOptions() { + IsAscending = false, + SortField = SortField.CreatedDate + }, + LimitTo = 10, + Combination = FilterCombination.And, + Statements = new List() + { + new FilterStatementDto() + { + Comparison = FilterComparison.Equal, + Field = FilterField.AgeRating, + Value = (int) AgeRating.Unknown + string.Empty + } + } + }; + + var encodedFilter = SmartFilterHelper.Encode(filter); + Assert.Equal("name=Test&stmts=comparison%253D0%252Cfield%253D4%252Cvalue%253D0&sortOptions=sortField%3D2%26isAscending%3DFalse&limitTo=10&combination=1", encodedFilter); } private void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value) diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index 69bfdf0bb..5bdd3eb6e 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Linq; using Xunit; using static API.Services.Tasks.Scanner.Parser.Parser; @@ -6,6 +7,14 @@ namespace API.Tests.Parser; public class ParserTests { + [Fact] + public void ShouldWork() + { + var s = 6.5f + ""; + var a = float.Parse(s, CultureInfo.InvariantCulture); + Assert.Equal(6.5f, a); + } + [Theory] [InlineData("Joe Shmo, Green Blue", "Joe Shmo, Green Blue")] [InlineData("Shmo, Joe", "Shmo, Joe")] diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 5a7046a7e..1200c3097 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Data.Common; +using System.Globalization; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; @@ -1219,7 +1220,7 @@ public class ReaderServiceTests var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,5, 1); var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter); - Assert.Equal(1, float.Parse(chapterInfoDto.ChapterNumber)); + Assert.Equal(1, chapterInfoDto.ChapterNumber.AsFloat()); // This is first chapter of first volume prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,4, 1); diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index f0ff0ed6f..a48532c32 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -380,6 +380,16 @@ public class AccountController : BaseApiController var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); _logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + if (!_emailService.IsValidEmail(user.Email)) + { + _logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email); + return Ok(new InviteUserResponse + { + EmailLink = string.Empty, + EmailSent = false + }); + } + var accessible = await _accountService.CheckIfAccessible(Request); if (accessible) @@ -572,28 +582,29 @@ public class AccountController : BaseApiController [HttpPost("invite")] public async Task> InviteUser(InviteUserDto dto) { - var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - if (adminUser == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); + var userId = User.GetUserId(); + var adminUser = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (adminUser == null) return Unauthorized(await _localizationService.Translate(userId, "permission-denied")); + + dto.Email = dto.Email.Trim(); + if (string.IsNullOrEmpty(dto.Email)) + return BadRequest(await _localizationService.Translate(userId, "invalid-payload")); _logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email); // Check if there is an existing invite - if (!string.IsNullOrEmpty(dto.Email)) + var emailValidationErrors = await _accountService.ValidateEmail(dto.Email); + if (emailValidationErrors.Any()) { - dto.Email = dto.Email.Trim(); - var emailValidationErrors = await _accountService.ValidateEmail(dto.Email); - if (emailValidationErrors.Any()) - { - var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); - if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName)); - return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited")); - } + var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) + return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName)); + return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited")); } // Create a new user var user = new AppUserBuilder(dto.Email, dto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); - + _unitOfWork.UserRepository.Add(user); try { var result = await _userManager.CreateAsync(user, AccountService.DefaultPassword); @@ -663,6 +674,17 @@ public class AccountController : BaseApiController var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); _logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink); _logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken); + + if (!_emailService.IsValidEmail(dto.Email)) + { + _logger.LogInformation("[Invite User] {Email} doesn't appear to be an email, so will not send an email to address", dto.Email); + return Ok(new InviteUserResponse + { + EmailLink = emailLink, + EmailSent = false + }); + } + var accessible = await _accountService.CheckIfAccessible(Request); if (accessible) { @@ -854,6 +876,11 @@ public class AccountController : BaseApiController var token = await _userManager.GeneratePasswordResetTokenAsync(user); var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); _logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + if (!_emailService.IsValidEmail(user.Email)) + { + _logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send", user.Email); + return Ok(await _localizationService.Translate(user.Id, "invalid-email")); + } if (await _accountService.CheckIfAccessible(Request)) { await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto() @@ -929,6 +956,13 @@ public class AccountController : BaseApiController var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email); _logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink); _logger.LogCritical("[Email Migration]: Token {UserName}: {Token}", user.UserName, token); + + if (!_emailService.IsValidEmail(user.Email)) + { + _logger.LogCritical("[Email Migration]: User is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.Email); + return Ok(await _localizationService.Translate(user.Id, "invalid-email")); + } + if (await _accountService.CheckIfAccessible(Request)) { try diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs index 8dcd3749d..da484981c 100644 --- a/API/Controllers/ImageController.cs +++ b/API/Controllers/ImageController.cs @@ -223,6 +223,7 @@ public class ImageController : BaseApiController var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId == 0) return BadRequest(); if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(userId, "must-be-defined", "Url")); + var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; // Check if the domain exists diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index a4f2a98b8..80cf05009 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -458,6 +458,8 @@ public class LibraryController : BaseApiController } await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(library.Id, "update"), false); + await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + MessageFactory.SideNavUpdateEvent(User.GetUserId()), false); await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index a4a2dd5d6..92597f903 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -775,7 +776,7 @@ public class OpdsController : BaseApiController var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId); foreach (var volume in seriesDetail.Volumes) { - var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number), + var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), _chapterSortComparer); foreach (var chapter in chapters) @@ -825,7 +826,7 @@ public class OpdsController : BaseApiController var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId); var chapters = - (await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number), + (await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), _chapterSortComparer); var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ", $"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix); diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs index 5ef57e907..49fb24474 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/API/Data/Metadata/ComicInfo.cs @@ -185,12 +185,12 @@ public class ComicInfo { try { - if (float.TryParse(Number, out var chpCount) && chpCount > 0) + if (float.TryParse(Number, CultureInfo.InvariantCulture, out var chpCount) && chpCount > 0) { return (int) Math.Floor(chpCount); } - if (float.TryParse(Volume, out var volCount) && volCount > 0) + if (float.TryParse(Volume, CultureInfo.InvariantCulture, out var volCount) && volCount > 0) { return (int) Math.Floor(volCount); } diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 755abca0e..d172ef8ba 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -43,12 +43,13 @@ public enum AppUserIncludes public interface IUserRepository { + void Add(AppUserBookmark bookmark); + void Add(AppUser bookmark); void Update(AppUser user); void Update(AppUserPreferences preferences); void Update(AppUserBookmark bookmark); void Update(AppUserDashboardStream stream); void Update(AppUserSideNavStream stream); - void Add(AppUserBookmark bookmark); void Delete(AppUser? user); void Delete(AppUserBookmark bookmark); void Delete(IEnumerable streams); @@ -108,6 +109,16 @@ public class UserRepository : IUserRepository _mapper = mapper; } + public void Add(AppUserBookmark bookmark) + { + _context.AppUserBookmark.Add(bookmark); + } + + public void Add(AppUser user) + { + _context.AppUser.Add(user); + } + public void Update(AppUser user) { _context.Entry(user).State = EntityState.Modified; @@ -133,11 +144,6 @@ public class UserRepository : IUserRepository _context.Entry(stream).State = EntityState.Modified; } - public void Add(AppUserBookmark bookmark) - { - _context.AppUserBookmark.Add(bookmark); - } - public void Delete(AppUser? user) { if (user == null) return; diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 30732fd99..3eadc3de0 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -5,6 +5,7 @@ using API.Entities; using API.Services; using AutoMapper; using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; namespace API.Data; diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index c0904e551..2ceeda942 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -107,7 +107,7 @@ public static class ApplicationServiceExtensions private static void AddSqLite(this IServiceCollection services) { - services.AddDbContext(options => + services.AddDbContextPool(options => { options.UseSqlite("Data source=config/kavita.db"); options.EnableDetailedErrors(); diff --git a/API/Extensions/SeriesExtensions.cs b/API/Extensions/SeriesExtensions.cs index 1e95d4261..5223f3120 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/API/Extensions/SeriesExtensions.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using System.Globalization; using System.Linq; using API.Comparators; using API.Entities; @@ -24,7 +25,7 @@ public static class SeriesExtensions if (firstVolume == null) return null; var chapters = firstVolume.Chapters - .OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default) + .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .ToList(); if (chapters.Count > 1 && chapters.Exists(c => c.IsSpecial)) @@ -44,9 +45,9 @@ public static class SeriesExtensions { var looseLeafChapters = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume) .SelectMany(c => c.Chapters.Where(c => !c.IsSpecial)) - .OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default) + .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .ToList(); - if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].Number) > float.Parse(looseLeafChapters[0].Number)) + if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].Number) > looseLeafChapters[0].Number.AsFloat()) { return looseLeafChapters[0].CoverImage; } @@ -56,7 +57,7 @@ public static class SeriesExtensions var firstLooseLeafChapter = volumes .Where(v => $"{v.Number}" == Parser.DefaultVolume) .SelectMany(v => v.Chapters) - .OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default) + .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .FirstOrDefault(c => !c.IsSpecial); return firstLooseLeafChapter?.CoverImage ?? firstVolume.CoverImage; diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs index 9ce29eaec..ae65ffe38 100644 --- a/API/Extensions/StringExtensions.cs +++ b/API/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Globalization; +using System.Text.RegularExpressions; namespace API.Extensions; @@ -22,4 +23,14 @@ public static class StringExtensions if (string.IsNullOrEmpty(value)) return string.Empty; return Services.Tasks.Scanner.Parser.Parser.Normalize(value); } + + public static float AsFloat(this string value) + { + return float.Parse(value, CultureInfo.InvariantCulture); + } + + public static double AsDouble(this string value) + { + return double.Parse(value, CultureInfo.InvariantCulture); + } } diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs index 35b115998..c4d6c5785 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using API.Entities; using API.Entities.Enums; using API.Services.Tasks.Scanner.Parser; @@ -17,7 +18,7 @@ public class ChapterBuilder : IEntityBuilder { Range = string.IsNullOrEmpty(range) ? number : range, Title = string.IsNullOrEmpty(range) ? number : range, - Number = Parser.MinNumberFromRange(number) + string.Empty, + Number = Parser.MinNumberFromRange(number).ToString(CultureInfo.InvariantCulture), Files = new List(), Pages = 1 }; diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/API/Helpers/Converters/FilterFieldValueConverter.cs index a09af509e..7f4001f67 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/API/Helpers/Converters/FilterFieldValueConverter.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using API.DTOs.Filtering.v2; using API.Entities.Enums; +using API.Extensions; namespace API.Helpers.Converters; @@ -68,7 +70,7 @@ public static class FilterFieldValueConverter .Select(int.Parse) .ToList(), FilterField.WantToRead => bool.Parse(value), - FilterField.ReadProgress => float.Parse(value), + FilterField.ReadProgress => value.AsFloat(), FilterField.ReadingDate => DateTime.Parse(value), FilterField.Formats => value.Split(',') .Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x)) diff --git a/API/Helpers/SmartFilterHelper.cs b/API/Helpers/SmartFilterHelper.cs index db30decf4..30b66c3ee 100644 --- a/API/Helpers/SmartFilterHelper.cs +++ b/API/Helpers/SmartFilterHelper.cs @@ -72,7 +72,7 @@ public static class SmartFilterHelper private static string EncodeSortOptions(SortOptions sortOptions) { - return $"sortField={(int) sortOptions.SortField}&isAscending={sortOptions.IsAscending}"; + return Uri.EscapeDataString($"sortField={(int) sortOptions.SortField}&isAscending={sortOptions.IsAscending}"); } private static string EncodeFilterStatementDtos(ICollection statements) @@ -90,7 +90,7 @@ public static class SmartFilterHelper var encodedField = $"field={(int) statement.Field}"; var encodedValue = $"value={Uri.EscapeDataString(statement.Value)}"; - return $"{encodedComparison}&{encodedField}&{encodedValue}"; + return Uri.EscapeDataString($"{encodedComparison},{encodedField},{encodedValue}"); } private static List DecodeFilterStatementDtos(string encodedStatements) @@ -119,11 +119,11 @@ public static class SmartFilterHelper private static SortOptions DecodeSortOptions(string encodedSortOptions) { - string[] parts = encodedSortOptions.Split('&'); + string[] parts = encodedSortOptions.Split(','); var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith("sortField=")); var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith("isAscending=")); - var isAscending = isAscendingPart?.Substring(11).Equals("true", StringComparison.OrdinalIgnoreCase) ?? true; + var isAscending = isAscendingPart?.Substring(11).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; if (sortFieldPart != null) { var sortField = Enum.Parse(sortFieldPart.Split("=")[1]); diff --git a/API/I18N/en.json b/API/I18N/en.json index f92fde286..861bebae1 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -31,6 +31,7 @@ "password-updated": "Password Updated", "forgot-password-generic": "An email will be sent to the email if it exists in our database", "not-accessible-password": "Your server is not accessible. The link to reset your password is in the logs", + "invalid-email": "The email on file for user is not a valid email. See logs for any links.", "not-accessible": "Your server is not accessible externally", "email-sent": "Email sent", "user-migration-needed": "This user needs to migrate. Have them log out and login to trigger a migration flow", diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 7a80d62d1..fe7df8815 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -10,6 +10,7 @@ using API.Data.Metadata; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Services.Tasks.Scanner.Parser; using Docnet.Core; using Docnet.Core.Converters; @@ -490,7 +491,7 @@ public class BookService : IBookService switch (metadataItem.Name) { case "calibre:rating": - info.UserRating = float.Parse(metadataItem.Content); + info.UserRating = metadataItem.Content.AsFloat(); break; case "calibre:title_sort": info.TitleSort = metadataItem.Content; @@ -649,7 +650,7 @@ public class BookService : IBookService return; case "ill": case "illustrator": - info.Letterer += AppendAuthor(person); + info.Inker += AppendAuthor(person); return; case "clr": case "colorist": diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 9b01f2eb3..78102e126 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -972,9 +972,9 @@ public class DirectoryService : IDirectoryService foreach (var file in directory.EnumerateFiles().OrderByNatural(file => file.FullName)) { if (file.Directory == null) continue; - var paddedIndex = Tasks.Scanner.Parser.Parser.PadZeros(directoryIndex + ""); + var paddedIndex = Tasks.Scanner.Parser.Parser.PadZeros(directoryIndex + string.Empty); // We need to rename the files so that after flattening, they are in the order we found them - var newName = $"{paddedIndex}_{Tasks.Scanner.Parser.Parser.PadZeros(fileIndex + "")}{file.Extension}"; + var newName = $"{paddedIndex}_{Tasks.Scanner.Parser.Parser.PadZeros(fileIndex + string.Empty)}{file.Extension}"; var newPath = Path.Join(root.FullName, newName); if (!File.Exists(newPath)) file.MoveTo(newPath); fileIndex++; diff --git a/API/Services/EmailService.cs b/API/Services/EmailService.cs index d002e74a4..e6f9c1684 100644 --- a/API/Services/EmailService.cs +++ b/API/Services/EmailService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -27,6 +28,7 @@ public interface IEmailService Task IsDefaultEmailService(); Task SendEmailChangeEmail(ConfirmationEmailDto data); Task GetVersion(string emailUrl); + bool IsValidEmail(string email); } public class EmailService : IEmailService @@ -123,6 +125,11 @@ public class EmailService : IEmailService return null; } + public bool IsValidEmail(string email) + { + return new EmailAddressAttribute().IsValid(email); + } + public async Task SendConfirmationEmail(ConfirmationEmailDto data) { var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; @@ -138,8 +145,15 @@ public class EmailService : IEmailService // This is the only exception for using the default because we need an external service to check if the server is accessible for emails try { - if (IsLocalIpAddress(host)) return false; - return await SendEmailWithGet(DefaultApiUrl + "/api/reachable?host=" + host); + if (IsLocalIpAddress(host)) + { + _logger.LogDebug("[EmailService] Server is not accessible, using local ip"); + return false; + } + + var url = DefaultApiUrl + "/api/reachable?host=" + host; + _logger.LogDebug("[EmailService] Checking if this server is accessible for sending an email to: {Url}", url); + return await SendEmailWithGet(url); } catch (Exception) { diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 05f0a4434..062b88935 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -288,7 +288,7 @@ public class ImageService : IImageService // Create the destination file path using var image = Image.PngloadStream(faviconStream); - var filename = $"{domain}{encodeFormat.GetExtension()}"; + var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); switch (encodeFormat) { case EncodeFormat.PNG: diff --git a/API/Services/MediaConversionService.cs b/API/Services/MediaConversionService.cs index 500e2706e..095509676 100644 --- a/API/Services/MediaConversionService.cs +++ b/API/Services/MediaConversionService.cs @@ -197,7 +197,7 @@ public class MediaConversionService : IMediaConversionService foreach (var volume in nonCustomOrConvertedVolumeCovers) { if (string.IsNullOrEmpty(volume.CoverImage)) continue; - volume.CoverImage = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage; + volume.CoverImage = volume.Chapters.MinBy(x => x.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)?.CoverImage; _unitOfWork.VolumeRepository.Update(volume); await _unitOfWork.CommitAsync(); } diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index f6fb06063..eba7977e8 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -107,7 +107,7 @@ public class MetadataService : IMetadataService volume.Chapters ??= new List(); - var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default); + var firstChapter = volume.Chapters.MinBy(x => x.Number.AsDouble(), ChapterSortComparerZeroFirst.Default); if (firstChapter == null) return Task.FromResult(false); volume.CoverImage = firstChapter.CoverImage; diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index cdee8932e..f9b206076 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -526,6 +526,7 @@ public class ScrobblingService : IScrobblingService foreach (var series in seriesWithProgress) { if (!libAllowsScrobbling[series.LibraryId]) continue; + if (series.PagesRead <= 0) continue; // Since we only scrobble when things are higher, we can await ScrobbleReadingUpdate(uId, series.Id); } diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index b4874b498..a56981069 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -245,6 +246,9 @@ public class ReaderService : IReaderService var userProgress = await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId); + // Don't create an empty progress record if there isn't any progress. This prevents Last Read date from being updated when + // opening a chapter + if (userProgress == null && progressDto.PageNum == 0) return true; if (userProgress == null) { @@ -367,16 +371,16 @@ public class ReaderService : IReaderService if (chapterId > 0) return chapterId; } - var currentVolumeNumber = float.Parse(currentVolume.Name); + var currentVolumeNumber = currentVolume.Name.AsFloat(); var next = false; foreach (var volume in volumes) { - var volumeNumbersMatch = Math.Abs(float.Parse(volume.Name) - currentVolumeNumber) < 0.00001f; + var volumeNumbersMatch = Math.Abs(volume.Name.AsFloat() - currentVolumeNumber) < 0.00001f; if (volumeNumbersMatch && volume.Chapters.Count > 1) { // Handle Chapters within current Volume // In this case, i need 0 first because 0 represents a full volume file. - var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer), + var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsFloat(), _chapterSortComparer), currentChapter.Range, dto => dto.Range); if (chapterId > 0) return chapterId; next = true; @@ -393,7 +397,7 @@ public class ReaderService : IReaderService // Handle Chapters within next Volume // ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+ - var chapters = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).ToList(); + var chapters = volume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer).ToList(); if (currentChapter.Number.Equals(Parser.DefaultChapter) && chapters.Last().Number.Equals(Parser.DefaultChapter)) { // We need to handle an extra check if the current chapter is the last special, as we should return -1 @@ -410,9 +414,9 @@ public class ReaderService : IReaderService var chapterId = GetNextChapterId(volume.Chapters.OrderByNatural(x => x.Number), currentChapter.Range, dto => dto.Range); if (chapterId > 0) return chapterId; - } else if (double.Parse(firstChapter.Number) >= double.Parse(currentChapter.Number)) return firstChapter.Id; + } else if (firstChapter.Number.AsDouble() >= currentChapter.Number.AsDouble()) return firstChapter.Id; // If we are the last chapter and next volume is there, we should try to use it (unless it's volume 0) - else if (double.Parse(firstChapter.Number) == 0) return firstChapter.Id; + else if (firstChapter.Number.AsDouble() == 0) return firstChapter.Id; // If on last volume AND there are no specials left, then let's return -1 var anySpecials = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume) @@ -439,16 +443,16 @@ public class ReaderService : IReaderService // if (currentVolume.Number == orderedVolumes.FirstOrDefault().Number) // { // // We can move into loose leaf chapters - // //var firstLooseLeaf = volumes.LastOrDefault().Chapters.MinBy(x => double.Parse(x.Number), _chapterSortComparer); + // //var firstLooseLeaf = volumes.LastOrDefault().Chapters.MinBy(x => x.Number.AsDouble(), _chapterSortComparer); // var nextChapterId = GetNextChapterId( - // volumes.LastOrDefault().Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer), + // volumes.LastOrDefault().Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer), // "0", dto => dto.Range); // // CHECK if we need a IsSpecial check // if (nextChapterId > 0) return nextChapterId; // } - var firstChapter = chapterVolume.Chapters.MinBy(x => double.Parse(x.Number), _chapterSortComparer); + var firstChapter = chapterVolume.Chapters.MinBy(x => x.Number.AsDouble(), _chapterSortComparer); if (firstChapter == null) return -1; @@ -486,7 +490,7 @@ public class ReaderService : IReaderService { if (volume.Number == currentVolume.Number) { - var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).Reverse(), + var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting).Reverse(), currentChapter.Range, dto => dto.Range); if (chapterId > 0) return chapterId; next = true; // When the diff between volumes is more than 1, we need to explicitly tell that next volume is our use case @@ -495,7 +499,7 @@ public class ReaderService : IReaderService if (next) { if (currentVolume.Number - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work - var lastChapter = volume.Chapters.MaxBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting); + var lastChapter = volume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); if (lastChapter == null) return -1; return lastChapter.Id; } @@ -504,7 +508,7 @@ public class ReaderService : IReaderService var lastVolume = volumes.MaxBy(v => v.Number); if (currentVolume.Number == 0 && currentVolume.Number != lastVolume?.Number && lastVolume?.Chapters.Count > 1) { - var lastChapter = lastVolume.Chapters.MaxBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting); + var lastChapter = lastVolume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); if (lastChapter == null) return -1; return lastChapter.Id; } @@ -527,8 +531,8 @@ public class ReaderService : IReaderService if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId)) { // I think i need a way to sort volumes last - return volumes.OrderBy(v => double.Parse(v.Number + string.Empty), _chapterSortComparer).First().Chapters - .OrderBy(c => float.Parse(c.Number)).First(); + return volumes.OrderBy(v => v.Number.ToString(CultureInfo.InvariantCulture).AsDouble(), _chapterSortComparer).First().Chapters + .OrderBy(c => c.Number.AsFloat()).First(); } // Loop through all chapters that are not in volume 0 @@ -540,13 +544,14 @@ public class ReaderService : IReaderService // NOTE: If volume 1 has chapter 1 and volume 2 is just chapter 0 due to being a full volume file, then this fails // If there are any volumes that have progress, return those. If not, move on. var currentlyReadingChapter = volumeChapters - .OrderBy(c => double.Parse(c.Number), _chapterSortComparer) + .OrderBy(c => c.Number.AsDouble(), _chapterSortComparer) .FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0); if (currentlyReadingChapter != null) return currentlyReadingChapter; // Order with volume 0 last so we prefer the natural order return FindNextReadingChapter(volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default) - .SelectMany(v => v.Chapters.OrderBy(c => double.Parse(c.Number))).ToList()); + .SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsDouble())) + .ToList()); } private static ChapterDto FindNextReadingChapter(IList volumeChapters) @@ -616,8 +621,8 @@ public class ReaderService : IReaderService foreach (var volume in volumes.OrderBy(v => v.Number)) { var chapters = volume.Chapters - .Where(c => !c.IsSpecial && Tasks.Scanner.Parser.Parser.MaxNumberFromRange(c.Range) <= chapterNumber) - .OrderBy(c => float.Parse(c.Number)); + .Where(c => !c.IsSpecial && Parser.MaxNumberFromRange(c.Range) <= chapterNumber) + .OrderBy(c => c.Number.AsFloat()); await MarkChaptersAsRead(user, volume.SeriesId, chapters.ToList()); } } diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index a48e5a4d7..66dc01431 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -12,6 +12,7 @@ using API.DTOs.ReadingLists; using API.DTOs.ReadingLists.CBL; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services.Tasks.Scanner.Parser; @@ -390,7 +391,7 @@ public class ReadingListService : IReadingListService var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet(); var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes)) .OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name)) - .ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting) + .ThenBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting) .ToList(); var index = readingList.Items.Count == 0 ? 0 : lastOrder + 1; diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 434862027..e505b610d 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -13,6 +13,7 @@ using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services.Plus; @@ -77,20 +78,22 @@ public class SeriesService : ISeriesService public static Chapter? GetFirstChapterForMetadata(Series series) { var sortedVolumes = series.Volumes - .Where(v => float.TryParse(v.Name, out var parsedValue) && parsedValue != 0.0f) - .OrderBy(v => float.TryParse(v.Name, out var parsedValue) ? parsedValue : float.MaxValue); + .Where(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) && parsedValue != 0.0f) + .OrderBy(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) ? parsedValue : float.MaxValue); var minVolumeNumber = sortedVolumes - .MinBy(v => float.Parse(v.Name)); + .MinBy(v => v.Name.AsFloat()); var allChapters = series.Volumes - .SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default)).ToList(); - var minChapter = allChapters + .SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)) + .ToList(); + var minChapter = allChapters .FirstOrDefault(); - if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, out var chapNum) && chapNum >= minVolumeNumber.Number) + if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, CultureInfo.InvariantCulture, out var chapNum) && + (chapNum >= minVolumeNumber.Number || chapNum == 0)) { - return minVolumeNumber.Chapters.MinBy(c => float.Parse(c.Number), ChapterSortComparer.Default); + return minVolumeNumber.Chapters.MinBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); } return minChapter; @@ -436,7 +439,9 @@ public class SeriesService : ISeriesService var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); foreach (var volume in volumes) { - volume.Chapters = volume.Chapters.OrderBy(d => double.Parse(d.Number), ChapterSortComparer.Default).ToList(); + volume.Chapters = volume.Chapters + .OrderBy(d => d.Number.AsDouble(), ChapterSortComparer.Default) + .ToList(); var firstChapter = volume.Chapters.First(); // On Books, skip volumes that are specials, since these will be shown if (firstChapter.IsSpecial) continue; @@ -450,7 +455,7 @@ public class SeriesService : ISeriesService processedVolumes.ForEach(v => { v.Name = $"Volume {v.Name}"; - v.Chapters = v.Chapters.OrderBy(d => double.Parse(d.Number), ChapterSortComparer.Default).ToList(); + v.Chapters = v.Chapters.OrderBy(d => d.Number.AsDouble(), ChapterSortComparer.Default).ToList(); }); } @@ -460,7 +465,7 @@ public class SeriesService : ISeriesService if (v.Number == 0) return c; c.VolumeTitle = v.Name; return c; - }).OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default)).ToList(); + }).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList(); foreach (var chapter in chapters) { @@ -485,12 +490,12 @@ public class SeriesService : ISeriesService var storylineChapters = volumes .Where(v => v.Number == 0) .SelectMany(v => v.Chapters.Where(c => !c.IsSpecial)) - .OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default) + .OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default) .ToList(); // When there's chapters without a volume number revert to chapter sorting only as opposed to volume then chapter if (storylineChapters.Any()) { - retChapters = retChapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default); + retChapters = retChapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); } return new SeriesDetailDto() @@ -717,33 +722,8 @@ public class SeriesService : ISeriesService ? chapters.Max(c => c.CreatedUtc) + TimeSpan.FromDays(forecastedTimeDifference) : (DateTime?)null; - // if (nextChapterExpected != null && nextChapterExpected < DateTime.UtcNow) - // { - // nextChapterExpected = DateTime.UtcNow + TimeSpan.FromDays(forecastedTimeDifference); - // } - // - // var averageTimeDifference = timeDifferences - // .Average(td => td.TotalDays); - // - // - // if (averageTimeDifference == 0) - // { - // return _emptyExpectedChapter; - // } - // - // - // // Calculate the forecast for when the next chapter is expected - // var nextChapterExpected = chapters.Any() - // ? chapters.Max(c => c.CreatedUtc) + TimeSpan.FromDays(averageTimeDifference) - // : (DateTime?) null; - // - // if (nextChapterExpected != null && nextChapterExpected < DateTime.UtcNow) - // { - // nextChapterExpected = DateTime.UtcNow + TimeSpan.FromDays(averageTimeDifference); - // } - // For number and volume number, we need the highest chapter, not the latest created - var lastChapter = chapters.MaxBy(c => float.Parse(c.Number))!; + var lastChapter = chapters.MaxBy(c => c.Number.AsFloat())!; float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture, out var lastChapterNumber); @@ -759,7 +739,7 @@ public class SeriesService : ISeriesService if (lastChapterNumber > 0) { - result.ChapterNumber = lastChapterNumber + 1; + result.ChapterNumber = (int) Math.Truncate(lastChapterNumber) + 1; result.VolumeNumber = lastChapter.Volume.Number; result.Title = series.Library.Type switch { @@ -783,9 +763,9 @@ public class SeriesService : ISeriesService return result; } - private double ExponentialSmoothing(IEnumerable data, double alpha) + private static double ExponentialSmoothing(IList data, double alpha) { - double forecast = data.First(); + var forecast = data.First(); foreach (var value in data) { diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index 813792c7f..3494f0bf6 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using API.Comparators; using API.Entities; +using API.Extensions; using AutoMapper; using Microsoft.Extensions.Logging; @@ -70,7 +71,10 @@ public class TachiyomiService : ITachiyomiService var looseLeafChapterVolume = volumes.Find(v => v.Number == 0); if (looseLeafChapterVolume == null) { - var volumeChapter = _mapper.Map(volumes.Last().Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparerZeroFirst.Default).Last()); + var volumeChapter = _mapper.Map(volumes + .Last().Chapters + .OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default) + .Last()); if (volumeChapter.Number == "0") { var volume = volumes.First(v => v.Id == volumeChapter.VolumeId); @@ -88,7 +92,9 @@ public class TachiyomiService : ITachiyomiService }; } - var lastChapter = looseLeafChapterVolume.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default).Last(); + var lastChapter = looseLeafChapterVolume.Chapters + .OrderBy(c => double.Parse(c.Number, CultureInfo.InvariantCulture), ChapterSortComparer.Default) + .Last(); return _mapper.Map(lastChapter); } diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs index b3408e93f..4ebbf57c6 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs @@ -194,7 +194,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService _logger.LogError(ex, "There was an error reading an epub file for word count, series skipped"); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent("There was an issue counting words on an epub", - $"{series.Name} - {file}")); + $"{series.Name} - {file.FilePath}")); return; } diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 5e5fa344b..f898d77cc 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -279,9 +279,23 @@ public class ParseScannedFiles IEnumerable folders, string libraryName, bool isLibraryScan, IDictionary> seriesPaths, Func>, Task>? processSeriesInfos, bool forceCheck = false) { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", libraryName, ProgressEventType.Started)); + foreach (var folderPath in folders) + { + try + { + await ProcessFiles(folderPath, isLibraryScan, seriesPaths, ProcessFolder, forceCheck); + } + catch (ArgumentException ex) + { + _logger.LogError(ex, "[ScannerService] The directory '{FolderPath}' does not exist", folderPath); + } + } + + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", libraryName, ProgressEventType.Ended)); + return; + async Task ProcessFolder(IList files, string folder) { var normalizedFolder = Parser.Parser.NormalizePath(folder); @@ -340,21 +354,6 @@ public class ParseScannedFiles } } } - - - foreach (var folderPath in folders) - { - try - { - await ProcessFiles(folderPath, isLibraryScan, seriesPaths, ProcessFolder, forceCheck); - } - catch (ArgumentException ex) - { - _logger.LogError(ex, "[ScannerService] The directory '{FolderPath}' does not exist", folderPath); - } - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", libraryName, ProgressEventType.Ended)); } /// diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 0f4095582..188afc9c1 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -38,7 +38,7 @@ public class DefaultParser : IDefaultParser ParserInfo ret; - if (Parser.IsEpub(filePath)) + if (Parser.IsEpub(filePath)) // NOTE: Will this ever be called? Because we use ReadingService to handle parse { ret = new ParserInfo { diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 8dfe314a6..4f126b8f1 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using API.Entities.Enums; +using API.Extensions; namespace API.Services.Tasks.Scanner.Parser; @@ -927,7 +928,7 @@ public static class Parser } var tokens = range.Replace("_", string.Empty).Split("-"); - return tokens.Min(float.Parse); + return tokens.Min(t => t.AsFloat()); } catch { @@ -945,7 +946,7 @@ public static class Parser } var tokens = range.Replace("_", string.Empty).Split("-"); - return tokens.Max(float.Parse); + return tokens.Max(t => t.AsFloat()); } catch { diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index c30a396e1..b42acafe7 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -21,6 +22,8 @@ using Microsoft.Extensions.Logging; namespace API.Services.Tasks.Scanner; +#nullable enable + public interface IProcessSeries { /// @@ -208,7 +211,7 @@ public class ProcessSeries : IProcessSeries .ToList()})); await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent($"There was an issue writing to the DB for Series {series}", + MessageFactory.ErrorEvent($"There was an issue writing to the DB for Series {series.OriginalName}", ex.Message)); return; } @@ -614,7 +617,7 @@ public class ProcessSeries : IProcessSeries // Add files var specialTreatment = info.IsSpecialInfo(); AddOrUpdateFileForChapter(chapter, info, forceUpdate); - chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty; + chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture); chapter.Range = specialTreatment ? info.Filename : info.Chapters; } @@ -886,7 +889,7 @@ public class ProcessSeries : IProcessSeries } } - action(genre, newTag); + action(genre!, newTag); } } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 7f41f685a..e42ba42cc 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -240,27 +240,6 @@ public class ScannerService : IScannerService await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Started, series.Name)); await _processSeries.Prime(); - async Task TrackFiles(Tuple> parsedInfo) - { - var parsedFiles = parsedInfo.Item2; - if (parsedFiles.Count == 0) return; - - var foundParsedSeries = new ParsedSeries() - { - Name = parsedFiles[0].Series, - NormalizedName = parsedFiles[0].Series.ToNormalized(), - Format = parsedFiles[0].Format - }; - - // For Scan Series, we need to filter out anything that isn't our Series - if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName) && !foundParsedSeries.NormalizedName.Equals(series.OriginalName?.ToNormalized())) - { - return; - } - - await _processSeries.ProcessSeriesAsync(parsedFiles, library, bypassFolderOptimizationChecks); - parsedSeries.Add(foundParsedSeries, parsedFiles); - } _logger.LogInformation("Beginning file scan on {SeriesName}", series.Name); var scanElapsedTime = await ScanFiles(library, new []{ folderPath }, false, TrackFiles, true); @@ -317,6 +296,29 @@ public class ScannerService : IScannerService BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, false)); BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)); + return; + + async Task TrackFiles(Tuple> parsedInfo) + { + var parsedFiles = parsedInfo.Item2; + if (parsedFiles.Count == 0) return; + + var foundParsedSeries = new ParsedSeries() + { + Name = parsedFiles[0].Series, + NormalizedName = parsedFiles[0].Series.ToNormalized(), + Format = parsedFiles[0].Format + }; + + // For Scan Series, we need to filter out anything that isn't our Series + if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName) && !foundParsedSeries.NormalizedName.Equals(series.OriginalName?.ToNormalized())) + { + return; + } + + await _processSeries.ProcessSeriesAsync(parsedFiles, library, bypassFolderOptimizationChecks); + parsedSeries.Add(foundParsedSeries, parsedFiles); + } } private async Task ShouldScanSeries(int seriesId, Library library, IList libraryPaths, Series series, bool bypassFolderChecks = false) @@ -488,38 +490,6 @@ public class ScannerService : IScannerService await _processSeries.Prime(); var processTasks = new List>(); - Task TrackFiles(Tuple> parsedInfo) - { - var skippedScan = parsedInfo.Item1; - var parsedFiles = parsedInfo.Item2; - if (parsedFiles.Count == 0) return Task.CompletedTask; - - var foundParsedSeries = new ParsedSeries() - { - Name = parsedFiles[0].Series, - NormalizedName = Scanner.Parser.Parser.Normalize(parsedFiles[0].Series), - Format = parsedFiles[0].Format - }; - - if (skippedScan) - { - seenSeries.AddRange(parsedFiles.Select(pf => new ParsedSeries() - { - Name = pf.Series, - NormalizedName = Scanner.Parser.Parser.Normalize(pf.Series), - Format = pf.Format - })); - return Task.CompletedTask; - } - - totalFiles += parsedFiles.Count; - - - seenSeries.Add(foundParsedSeries); - processTasks.Add(async () => await _processSeries.ProcessSeriesAsync(parsedFiles, library, forceUpdate)); - return Task.CompletedTask; - } - var scanElapsedTime = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, TrackFiles, forceUpdate); // NOTE: This runs sync after every file is scanned @@ -592,6 +562,39 @@ public class ScannerService : IScannerService await _metadataService.RemoveAbandonedMetadataKeys(); BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)); + return; + + Task TrackFiles(Tuple> parsedInfo) + { + var skippedScan = parsedInfo.Item1; + var parsedFiles = parsedInfo.Item2; + if (parsedFiles.Count == 0) return Task.CompletedTask; + + var foundParsedSeries = new ParsedSeries() + { + Name = parsedFiles[0].Series, + NormalizedName = Scanner.Parser.Parser.Normalize(parsedFiles[0].Series), + Format = parsedFiles[0].Format + }; + + if (skippedScan) + { + seenSeries.AddRange(parsedFiles.Select(pf => new ParsedSeries() + { + Name = pf.Series, + NormalizedName = Scanner.Parser.Parser.Normalize(pf.Series), + Format = pf.Format + })); + return Task.CompletedTask; + } + + totalFiles += parsedFiles.Count; + + + seenSeries.Add(foundParsedSeries); + processTasks.Add(async () => await _processSeries.ProcessSeriesAsync(parsedFiles, library, forceUpdate)); + return Task.CompletedTask; + } } private async Task ScanFiles(Library library, IEnumerable dirs, diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 9d2089aba..03e677260 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -998,6 +998,7 @@ "version": "16.2.9", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.9.tgz", "integrity": "sha512-ecH2oOlijJdDqioD9IfgdqJGoRRHI6hAx5rwBxIaYk01ywj13KzvXWPrXbCIupeWtV/XUZUlbwf47nlmL5gxZg==", + "dev": true, "dependencies": { "@babel/core": "7.22.5", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -5897,6 +5898,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" @@ -6152,6 +6154,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" } @@ -6459,6 +6462,7 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, "funding": [ { "type": "individual", @@ -7638,6 +7642,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" @@ -7647,6 +7652,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" @@ -8743,6 +8749,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -9566,6 +9573,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" }, @@ -11404,6 +11412,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" } @@ -12744,6 +12753,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" }, @@ -12754,7 +12764,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", @@ -13190,7 +13201,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.64.1", @@ -13317,6 +13328,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" }, @@ -13331,6 +13343,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" }, @@ -13341,7 +13354,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", @@ -14457,6 +14471,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/UI/Web/src/app/_guards/admin.guard.ts b/UI/Web/src/app/_guards/admin.guard.ts index 9e34ff5eb..9de2cfe44 100644 --- a/UI/Web/src/app/_guards/admin.guard.ts +++ b/UI/Web/src/app/_guards/admin.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { CanActivate } from '@angular/router'; +import {CanActivate, Router} from '@angular/router'; import { ToastrService } from 'ngx-toastr'; import { Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -11,10 +11,10 @@ import {TranslocoService} from "@ngneat/transloco"; }) export class AdminGuard implements CanActivate { constructor(private accountService: AccountService, private toastr: ToastrService, + private router: Router, private translocoService: TranslocoService) {} canActivate(): Observable { - // this automatically subs due to being router guard return this.accountService.currentUser$.pipe(take(1), map((user) => { if (user && this.accountService.hasAdminRole(user)) { @@ -22,6 +22,7 @@ export class AdminGuard implements CanActivate { } this.toastr.error(this.translocoService.translate('toasts.unauthorized-1')); + this.router.navigateByUrl('/libraries'); return false; }) ); diff --git a/UI/Web/src/app/_guards/auth.guard.ts b/UI/Web/src/app/_guards/auth.guard.ts index 8ef7e5912..5d403469e 100644 --- a/UI/Web/src/app/_guards/auth.guard.ts +++ b/UI/Web/src/app/_guards/auth.guard.ts @@ -22,12 +22,7 @@ export class AuthGuard implements CanActivate { if (user) { return true; } - // TODO: Remove the error message stuff here and just redirect them. Don't need to tell them - const errorMessage = this.translocoService.translate('toasts.unauthorized-1'); - const errorMessage2 = this.translocoService.translate('toasts.unauthorized-2'); - if (this.toastr.toasts.filter(toast => toast.message === errorMessage2 || toast.message === errorMessage).length === 0) { - this.toastr.error(errorMessage); - } + localStorage.setItem(this.urlKey, window.location.pathname); this.router.navigateByUrl('/login'); return false; diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html index 5ad1f6132..1b1195946 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html @@ -124,9 +124,9 @@
- + (newItemAdded)="metadata.tagsLocked = true"> {{item.title}} diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index 1a649dd77..68cb3f758 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -368,7 +368,7 @@ export class EditSeriesModalComponent implements OnInit { return {id: 0, title: title }; }); this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => { - return a.id == b.id; + return a.title.toLowerCase() == b.title.toLowerCase(); } this.tagsSettings.compareFnForAdd = (options: Tag[], filter: string) => { return options.filter(m => this.utilityService.filterMatches(m.title, filter)); @@ -398,7 +398,7 @@ export class EditSeriesModalComponent implements OnInit { return options.filter(m => this.utilityService.filterMatches(m.title, filter)); } this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => { - return a.title == b.title; + return a.title.toLowerCase() == b.title.toLowerCase(); } this.genreSettings.addTransformFn = ((title: string) => { diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts index 59c229c40..1a0774b20 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts @@ -103,11 +103,10 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy { } ctx.drawImage(img, 0, 0); - const dataURL = canvas.toDataURL("image/png"); - return dataURL; + return canvas.toDataURL("image/png"); } - selectImage(index: number) { + selectImage(index: number, callback?: Function) { if (this.selectedIndex === index) { return; } // If we load custom images of series/chapters/covers, then those urls are not properly encoded, so on select we have to clean them up @@ -116,7 +115,11 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy { const img = new Image(); img.crossOrigin = 'Anonymous'; img.src = imgUrl; - img.onload = (e) => this.handleUrlImageAdd(img, index); + img.onload = (e) => { + this.handleUrlImageAdd(img, index); + this.selectedBase64Url.emit(this.imageUrls[this.selectedIndex]); + if (callback) callback(index); + }; img.onerror = (e) => { this.toastr.error(translate('errors.rejected-cover-upload')); this.form.get('coverImageUrl')?.setValue(''); @@ -124,7 +127,6 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy { }; this.form.get('coverImageUrl')?.setValue(''); this.cdRef.markForCheck(); - this.selectedBase64Url.emit(this.imageUrls[this.selectedIndex]); return; } @@ -135,11 +137,13 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy { } applyImage(index: number) { - if (this.showApplyButton) { + if (!this.showApplyButton) return; + + this.selectImage(index, () => { this.applyCover.emit(this.imageUrls[index]); this.appliedIndex = index; this.cdRef.markForCheck(); - } + }); } resetImage() { diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index fb3d5bc5e..05e4c2cc5 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -1645,6 +1645,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { data.emulateBook = modelSettings.emulateBook; data.swipeToPaginate = modelSettings.swipeToPaginate; data.pageSplitOption = parseInt(modelSettings.pageSplitOption, 10); + data.locale = data.locale || 'en'; this.accountService.updatePreferences(data).subscribe(updatedPrefs => { this.toastr.success(translate('manga-reader.user-preferences-updated')); diff --git a/UI/Web/src/app/registration/user-login/user-login.component.ts b/UI/Web/src/app/registration/user-login/user-login.component.ts index 554b48c3a..6c296b5ad 100644 --- a/UI/Web/src/app/registration/user-login/user-login.component.ts +++ b/UI/Web/src/app/registration/user-login/user-login.component.ts @@ -53,7 +53,6 @@ export class UserLoginComponent implements OnInit { if (user) { this.navService.showSideNav(); this.cdRef.markForCheck(); - this.router.navigateByUrl('/libraries'); } }); @@ -96,6 +95,7 @@ export class UserLoginComponent implements OnInit { localStorage.setItem('kavita--auth-intersection-url', ''); this.router.navigateByUrl(pageResume); } else { + localStorage.setItem('kavita--auth-intersection-url', ''); this.router.navigateByUrl('/libraries'); } this.isSubmitting = false; diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html index 37f2b4c49..ac797ccfb 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html @@ -15,7 +15,7 @@ -