mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Lots of Bugfixes (#2356)
This commit is contained in:
parent
86e931dd9a
commit
226d6831df
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
@ -31,7 +32,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Special 1", series.GetCoverImage());
|
||||||
@ -66,7 +67,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage());
|
||||||
@ -108,7 +109,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage());
|
||||||
@ -134,7 +135,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Special 2", series.GetCoverImage());
|
||||||
@ -164,7 +165,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Chapter 2", series.GetCoverImage());
|
||||||
@ -201,7 +202,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||||
@ -238,7 +239,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||||
@ -282,7 +283,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||||
@ -315,7 +316,7 @@ public class SeriesExtensionsTests
|
|||||||
|
|
||||||
foreach (var vol in series.Volumes)
|
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());
|
Assert.Equal("Chapter 2", series.GetCoverImage());
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
using System.Linq;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.Filtering.v2;
|
using API.DTOs.Filtering.v2;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Helpers;
|
namespace API.Tests.Helpers;
|
||||||
@ -13,21 +16,47 @@ public class SmartFilterHelperTests
|
|||||||
public void Test_Decode()
|
public void Test_Decode()
|
||||||
{
|
{
|
||||||
var encoded = """
|
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);
|
var filter = SmartFilterHelper.Decode(encoded);
|
||||||
|
|
||||||
Assert.Equal(0, filter.LimitTo);
|
Assert.Equal(10, filter.LimitTo);
|
||||||
Assert.Equal(SortField.SortName, filter.SortOptions.SortField);
|
Assert.Equal(SortField.CreatedDate, filter.SortOptions.SortField);
|
||||||
Assert.True(filter.SortOptions.IsAscending);
|
Assert.False(filter.SortOptions.IsAscending);
|
||||||
Assert.Null(filter.Name);
|
Assert.Null(filter.Name);
|
||||||
|
|
||||||
var list = filter.Statements.ToList();
|
var list = filter.Statements.ToList();
|
||||||
AssertStatementSame(list[2], FilterField.SeriesName, FilterComparison.Matches, "a");
|
AssertStatementSame(list[2], FilterField.SeriesName, FilterComparison.Matches, "a");
|
||||||
AssertStatementSame(list[1], FilterField.AgeRating, FilterComparison.Equal, (int) AgeRating.Unknown + "");
|
AssertStatementSame(list[1], FilterField.AgeRating, FilterComparison.Equal, (int) AgeRating.Unknown + string.Empty);
|
||||||
AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Contains, "6");
|
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<FilterStatementDto>()
|
||||||
|
{
|
||||||
|
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)
|
private void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using static API.Services.Tasks.Scanner.Parser.Parser;
|
using static API.Services.Tasks.Scanner.Parser.Parser;
|
||||||
@ -6,6 +7,14 @@ namespace API.Tests.Parser;
|
|||||||
|
|
||||||
public class ParserTests
|
public class ParserTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ShouldWork()
|
||||||
|
{
|
||||||
|
var s = 6.5f + "";
|
||||||
|
var a = float.Parse(s, CultureInfo.InvariantCulture);
|
||||||
|
Assert.Equal(6.5f, a);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Joe Shmo, Green Blue", "Joe Shmo, Green Blue")]
|
[InlineData("Joe Shmo, Green Blue", "Joe Shmo, Green Blue")]
|
||||||
[InlineData("Shmo, Joe", "Shmo, Joe")]
|
[InlineData("Shmo, Joe", "Shmo, Joe")]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -1219,7 +1220,7 @@ public class ReaderServiceTests
|
|||||||
|
|
||||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,5, 1);
|
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,5, 1);
|
||||||
var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter);
|
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
|
// This is first chapter of first volume
|
||||||
prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,4, 1);
|
prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,4, 1);
|
||||||
|
@ -380,6 +380,16 @@ public class AccountController : BaseApiController
|
|||||||
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email);
|
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);
|
_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);
|
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||||
if (accessible)
|
if (accessible)
|
||||||
@ -572,28 +582,29 @@ public class AccountController : BaseApiController
|
|||||||
[HttpPost("invite")]
|
[HttpPost("invite")]
|
||||||
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
||||||
{
|
{
|
||||||
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var userId = User.GetUserId();
|
||||||
if (adminUser == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
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);
|
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
||||||
|
|
||||||
// Check if there is an existing invite
|
// 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 invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||||
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
|
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||||
if (emailValidationErrors.Any())
|
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
|
// Create a new user
|
||||||
var user = new AppUserBuilder(dto.Email, dto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
var user = new AppUserBuilder(dto.Email, dto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
||||||
|
_unitOfWork.UserRepository.Add(user);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _userManager.CreateAsync(user, AccountService.DefaultPassword);
|
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);
|
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]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||||
_logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken);
|
_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);
|
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||||
if (accessible)
|
if (accessible)
|
||||||
{
|
{
|
||||||
@ -854,6 +876,11 @@ public class AccountController : BaseApiController
|
|||||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||||
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
|
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
|
||||||
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
_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))
|
if (await _accountService.CheckIfAccessible(Request))
|
||||||
{
|
{
|
||||||
await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto()
|
await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto()
|
||||||
@ -929,6 +956,13 @@ public class AccountController : BaseApiController
|
|||||||
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email);
|
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email);
|
||||||
_logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink);
|
_logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink);
|
||||||
_logger.LogCritical("[Email Migration]: Token {UserName}: {Token}", user.UserName, token);
|
_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))
|
if (await _accountService.CheckIfAccessible(Request))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -223,6 +223,7 @@ public class ImageController : BaseApiController
|
|||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||||
if (userId == 0) return BadRequest();
|
if (userId == 0) return BadRequest();
|
||||||
if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(userId, "must-be-defined", "Url"));
|
if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(userId, "must-be-defined", "Url"));
|
||||||
|
|
||||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||||
|
|
||||||
// Check if the domain exists
|
// Check if the domain exists
|
||||||
|
@ -458,6 +458,8 @@ public class LibraryController : BaseApiController
|
|||||||
}
|
}
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||||
MessageFactory.LibraryModifiedEvent(library.Id, "update"), false);
|
MessageFactory.LibraryModifiedEvent(library.Id, "update"), false);
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
|
||||||
|
MessageFactory.SideNavUpdateEvent(User.GetUserId()), false);
|
||||||
|
|
||||||
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
|
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -775,7 +776,7 @@ public class OpdsController : BaseApiController
|
|||||||
var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId);
|
var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||||
foreach (var volume in seriesDetail.Volumes)
|
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);
|
_chapterSortComparer);
|
||||||
|
|
||||||
foreach (var chapter in chapters)
|
foreach (var chapter in chapters)
|
||||||
@ -825,7 +826,7 @@ public class OpdsController : BaseApiController
|
|||||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
||||||
var chapters =
|
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);
|
_chapterSortComparer);
|
||||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ",
|
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ",
|
||||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
|
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
|
||||||
|
@ -185,12 +185,12 @@ public class ComicInfo
|
|||||||
{
|
{
|
||||||
try
|
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);
|
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);
|
return (int) Math.Floor(volCount);
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,13 @@ public enum AppUserIncludes
|
|||||||
|
|
||||||
public interface IUserRepository
|
public interface IUserRepository
|
||||||
{
|
{
|
||||||
|
void Add(AppUserBookmark bookmark);
|
||||||
|
void Add(AppUser bookmark);
|
||||||
void Update(AppUser user);
|
void Update(AppUser user);
|
||||||
void Update(AppUserPreferences preferences);
|
void Update(AppUserPreferences preferences);
|
||||||
void Update(AppUserBookmark bookmark);
|
void Update(AppUserBookmark bookmark);
|
||||||
void Update(AppUserDashboardStream stream);
|
void Update(AppUserDashboardStream stream);
|
||||||
void Update(AppUserSideNavStream stream);
|
void Update(AppUserSideNavStream stream);
|
||||||
void Add(AppUserBookmark bookmark);
|
|
||||||
void Delete(AppUser? user);
|
void Delete(AppUser? user);
|
||||||
void Delete(AppUserBookmark bookmark);
|
void Delete(AppUserBookmark bookmark);
|
||||||
void Delete(IEnumerable<AppUserDashboardStream> streams);
|
void Delete(IEnumerable<AppUserDashboardStream> streams);
|
||||||
@ -108,6 +109,16 @@ public class UserRepository : IUserRepository
|
|||||||
_mapper = mapper;
|
_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)
|
public void Update(AppUser user)
|
||||||
{
|
{
|
||||||
_context.Entry(user).State = EntityState.Modified;
|
_context.Entry(user).State = EntityState.Modified;
|
||||||
@ -133,11 +144,6 @@ public class UserRepository : IUserRepository
|
|||||||
_context.Entry(stream).State = EntityState.Modified;
|
_context.Entry(stream).State = EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Add(AppUserBookmark bookmark)
|
|
||||||
{
|
|
||||||
_context.AppUserBookmark.Add(bookmark);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(AppUser? user)
|
public void Delete(AppUser? user)
|
||||||
{
|
{
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
@ -5,6 +5,7 @@ using API.Entities;
|
|||||||
using API.Services;
|
using API.Services;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Data;
|
namespace API.Data;
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ public static class ApplicationServiceExtensions
|
|||||||
|
|
||||||
private static void AddSqLite(this IServiceCollection services)
|
private static void AddSqLite(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddDbContext<DataContext>(options =>
|
services.AddDbContextPool<DataContext>(options =>
|
||||||
{
|
{
|
||||||
options.UseSqlite("Data source=config/kavita.db");
|
options.UseSqlite("Data source=config/kavita.db");
|
||||||
options.EnableDetailedErrors();
|
options.EnableDetailedErrors();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
@ -24,7 +25,7 @@ public static class SeriesExtensions
|
|||||||
if (firstVolume == null) return null;
|
if (firstVolume == null) return null;
|
||||||
|
|
||||||
var chapters = firstVolume.Chapters
|
var chapters = firstVolume.Chapters
|
||||||
.OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default)
|
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (chapters.Count > 1 && chapters.Exists(c => c.IsSpecial))
|
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)
|
var looseLeafChapters = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume)
|
||||||
.SelectMany(c => c.Chapters.Where(c => !c.IsSpecial))
|
.SelectMany(c => c.Chapters.Where(c => !c.IsSpecial))
|
||||||
.OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default)
|
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
|
||||||
.ToList();
|
.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;
|
return looseLeafChapters[0].CoverImage;
|
||||||
}
|
}
|
||||||
@ -56,7 +57,7 @@ public static class SeriesExtensions
|
|||||||
var firstLooseLeafChapter = volumes
|
var firstLooseLeafChapter = volumes
|
||||||
.Where(v => $"{v.Number}" == Parser.DefaultVolume)
|
.Where(v => $"{v.Number}" == Parser.DefaultVolume)
|
||||||
.SelectMany(v => v.Chapters)
|
.SelectMany(v => v.Chapters)
|
||||||
.OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default)
|
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
|
||||||
.FirstOrDefault(c => !c.IsSpecial);
|
.FirstOrDefault(c => !c.IsSpecial);
|
||||||
|
|
||||||
return firstLooseLeafChapter?.CoverImage ?? firstVolume.CoverImage;
|
return firstLooseLeafChapter?.CoverImage ?? firstVolume.CoverImage;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace API.Extensions;
|
namespace API.Extensions;
|
||||||
|
|
||||||
@ -22,4 +23,14 @@ public static class StringExtensions
|
|||||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||||
return Services.Tasks.Scanner.Parser.Parser.Normalize(value);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
@ -17,7 +18,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||||||
{
|
{
|
||||||
Range = string.IsNullOrEmpty(range) ? number : range,
|
Range = string.IsNullOrEmpty(range) ? number : range,
|
||||||
Title = 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<MangaFile>(),
|
Files = new List<MangaFile>(),
|
||||||
Pages = 1
|
Pages = 1
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.DTOs.Filtering.v2;
|
using API.DTOs.Filtering.v2;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
|
|
||||||
namespace API.Helpers.Converters;
|
namespace API.Helpers.Converters;
|
||||||
|
|
||||||
@ -68,7 +70,7 @@ public static class FilterFieldValueConverter
|
|||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToList(),
|
.ToList(),
|
||||||
FilterField.WantToRead => bool.Parse(value),
|
FilterField.WantToRead => bool.Parse(value),
|
||||||
FilterField.ReadProgress => float.Parse(value),
|
FilterField.ReadProgress => value.AsFloat(),
|
||||||
FilterField.ReadingDate => DateTime.Parse(value),
|
FilterField.ReadingDate => DateTime.Parse(value),
|
||||||
FilterField.Formats => value.Split(',')
|
FilterField.Formats => value.Split(',')
|
||||||
.Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x))
|
.Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x))
|
||||||
|
@ -72,7 +72,7 @@ public static class SmartFilterHelper
|
|||||||
|
|
||||||
private static string EncodeSortOptions(SortOptions sortOptions)
|
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<FilterStatementDto> statements)
|
private static string EncodeFilterStatementDtos(ICollection<FilterStatementDto> statements)
|
||||||
@ -90,7 +90,7 @@ public static class SmartFilterHelper
|
|||||||
var encodedField = $"field={(int) statement.Field}";
|
var encodedField = $"field={(int) statement.Field}";
|
||||||
var encodedValue = $"value={Uri.EscapeDataString(statement.Value)}";
|
var encodedValue = $"value={Uri.EscapeDataString(statement.Value)}";
|
||||||
|
|
||||||
return $"{encodedComparison}&{encodedField}&{encodedValue}";
|
return Uri.EscapeDataString($"{encodedComparison},{encodedField},{encodedValue}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<FilterStatementDto> DecodeFilterStatementDtos(string encodedStatements)
|
private static List<FilterStatementDto> DecodeFilterStatementDtos(string encodedStatements)
|
||||||
@ -119,11 +119,11 @@ public static class SmartFilterHelper
|
|||||||
|
|
||||||
private static SortOptions DecodeSortOptions(string encodedSortOptions)
|
private static SortOptions DecodeSortOptions(string encodedSortOptions)
|
||||||
{
|
{
|
||||||
string[] parts = encodedSortOptions.Split('&');
|
string[] parts = encodedSortOptions.Split(',');
|
||||||
var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith("sortField="));
|
var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith("sortField="));
|
||||||
var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith("isAscending="));
|
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)
|
if (sortFieldPart != null)
|
||||||
{
|
{
|
||||||
var sortField = Enum.Parse<SortField>(sortFieldPart.Split("=")[1]);
|
var sortField = Enum.Parse<SortField>(sortFieldPart.Split("=")[1]);
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"password-updated": "Password Updated",
|
"password-updated": "Password Updated",
|
||||||
"forgot-password-generic": "An email will be sent to the email if it exists in our database",
|
"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",
|
"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",
|
"not-accessible": "Your server is not accessible externally",
|
||||||
"email-sent": "Email sent",
|
"email-sent": "Email sent",
|
||||||
"user-migration-needed": "This user needs to migrate. Have them log out and login to trigger a migration flow",
|
"user-migration-needed": "This user needs to migrate. Have them log out and login to trigger a migration flow",
|
||||||
|
@ -10,6 +10,7 @@ using API.Data.Metadata;
|
|||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Docnet.Core;
|
using Docnet.Core;
|
||||||
using Docnet.Core.Converters;
|
using Docnet.Core.Converters;
|
||||||
@ -490,7 +491,7 @@ public class BookService : IBookService
|
|||||||
switch (metadataItem.Name)
|
switch (metadataItem.Name)
|
||||||
{
|
{
|
||||||
case "calibre:rating":
|
case "calibre:rating":
|
||||||
info.UserRating = float.Parse(metadataItem.Content);
|
info.UserRating = metadataItem.Content.AsFloat();
|
||||||
break;
|
break;
|
||||||
case "calibre:title_sort":
|
case "calibre:title_sort":
|
||||||
info.TitleSort = metadataItem.Content;
|
info.TitleSort = metadataItem.Content;
|
||||||
@ -649,7 +650,7 @@ public class BookService : IBookService
|
|||||||
return;
|
return;
|
||||||
case "ill":
|
case "ill":
|
||||||
case "illustrator":
|
case "illustrator":
|
||||||
info.Letterer += AppendAuthor(person);
|
info.Inker += AppendAuthor(person);
|
||||||
return;
|
return;
|
||||||
case "clr":
|
case "clr":
|
||||||
case "colorist":
|
case "colorist":
|
||||||
|
@ -972,9 +972,9 @@ public class DirectoryService : IDirectoryService
|
|||||||
foreach (var file in directory.EnumerateFiles().OrderByNatural(file => file.FullName))
|
foreach (var file in directory.EnumerateFiles().OrderByNatural(file => file.FullName))
|
||||||
{
|
{
|
||||||
if (file.Directory == null) continue;
|
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
|
// 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);
|
var newPath = Path.Join(root.FullName, newName);
|
||||||
if (!File.Exists(newPath)) file.MoveTo(newPath);
|
if (!File.Exists(newPath)) file.MoveTo(newPath);
|
||||||
fileIndex++;
|
fileIndex++;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -27,6 +28,7 @@ public interface IEmailService
|
|||||||
Task<bool> IsDefaultEmailService();
|
Task<bool> IsDefaultEmailService();
|
||||||
Task SendEmailChangeEmail(ConfirmationEmailDto data);
|
Task SendEmailChangeEmail(ConfirmationEmailDto data);
|
||||||
Task<string?> GetVersion(string emailUrl);
|
Task<string?> GetVersion(string emailUrl);
|
||||||
|
bool IsValidEmail(string email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EmailService : IEmailService
|
public class EmailService : IEmailService
|
||||||
@ -123,6 +125,11 @@ public class EmailService : IEmailService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsValidEmail(string email)
|
||||||
|
{
|
||||||
|
return new EmailAddressAttribute().IsValid(email);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SendConfirmationEmail(ConfirmationEmailDto data)
|
public async Task SendConfirmationEmail(ConfirmationEmailDto data)
|
||||||
{
|
{
|
||||||
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
|
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
|
// 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
|
try
|
||||||
{
|
{
|
||||||
if (IsLocalIpAddress(host)) return false;
|
if (IsLocalIpAddress(host))
|
||||||
return await SendEmailWithGet(DefaultApiUrl + "/api/reachable?host=" + 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)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
@ -288,7 +288,7 @@ public class ImageService : IImageService
|
|||||||
|
|
||||||
// Create the destination file path
|
// Create the destination file path
|
||||||
using var image = Image.PngloadStream(faviconStream);
|
using var image = Image.PngloadStream(faviconStream);
|
||||||
var filename = $"{domain}{encodeFormat.GetExtension()}";
|
var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat);
|
||||||
switch (encodeFormat)
|
switch (encodeFormat)
|
||||||
{
|
{
|
||||||
case EncodeFormat.PNG:
|
case EncodeFormat.PNG:
|
||||||
|
@ -197,7 +197,7 @@ public class MediaConversionService : IMediaConversionService
|
|||||||
foreach (var volume in nonCustomOrConvertedVolumeCovers)
|
foreach (var volume in nonCustomOrConvertedVolumeCovers)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(volume.CoverImage)) continue;
|
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);
|
_unitOfWork.VolumeRepository.Update(volume);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ public class MetadataService : IMetadataService
|
|||||||
|
|
||||||
|
|
||||||
volume.Chapters ??= new List<Chapter>();
|
volume.Chapters ??= new List<Chapter>();
|
||||||
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);
|
if (firstChapter == null) return Task.FromResult(false);
|
||||||
|
|
||||||
volume.CoverImage = firstChapter.CoverImage;
|
volume.CoverImage = firstChapter.CoverImage;
|
||||||
|
@ -526,6 +526,7 @@ public class ScrobblingService : IScrobblingService
|
|||||||
foreach (var series in seriesWithProgress)
|
foreach (var series in seriesWithProgress)
|
||||||
{
|
{
|
||||||
if (!libAllowsScrobbling[series.LibraryId]) continue;
|
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);
|
await ScrobbleReadingUpdate(uId, series.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -245,6 +246,9 @@ public class ReaderService : IReaderService
|
|||||||
var userProgress =
|
var userProgress =
|
||||||
await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId);
|
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)
|
if (userProgress == null)
|
||||||
{
|
{
|
||||||
@ -367,16 +371,16 @@ public class ReaderService : IReaderService
|
|||||||
if (chapterId > 0) return chapterId;
|
if (chapterId > 0) return chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentVolumeNumber = float.Parse(currentVolume.Name);
|
var currentVolumeNumber = currentVolume.Name.AsFloat();
|
||||||
var next = false;
|
var next = false;
|
||||||
foreach (var volume in volumes)
|
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)
|
if (volumeNumbersMatch && volume.Chapters.Count > 1)
|
||||||
{
|
{
|
||||||
// Handle Chapters within current Volume
|
// Handle Chapters within current Volume
|
||||||
// In this case, i need 0 first because 0 represents a full volume file.
|
// 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);
|
currentChapter.Range, dto => dto.Range);
|
||||||
if (chapterId > 0) return chapterId;
|
if (chapterId > 0) return chapterId;
|
||||||
next = true;
|
next = true;
|
||||||
@ -393,7 +397,7 @@ public class ReaderService : IReaderService
|
|||||||
|
|
||||||
// Handle Chapters within next Volume
|
// Handle Chapters within next Volume
|
||||||
// ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+
|
// ! 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))
|
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
|
// 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),
|
var chapterId = GetNextChapterId(volume.Chapters.OrderByNatural(x => x.Number),
|
||||||
currentChapter.Range, dto => dto.Range);
|
currentChapter.Range, dto => dto.Range);
|
||||||
if (chapterId > 0) return chapterId;
|
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)
|
// 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
|
// If on last volume AND there are no specials left, then let's return -1
|
||||||
var anySpecials = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume)
|
var anySpecials = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume)
|
||||||
@ -439,16 +443,16 @@ public class ReaderService : IReaderService
|
|||||||
// if (currentVolume.Number == orderedVolumes.FirstOrDefault().Number)
|
// if (currentVolume.Number == orderedVolumes.FirstOrDefault().Number)
|
||||||
// {
|
// {
|
||||||
// // We can move into loose leaf chapters
|
// // 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(
|
// 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);
|
// "0", dto => dto.Range);
|
||||||
// // CHECK if we need a IsSpecial check
|
// // CHECK if we need a IsSpecial check
|
||||||
// if (nextChapterId > 0) return nextChapterId;
|
// 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;
|
if (firstChapter == null) return -1;
|
||||||
|
|
||||||
|
|
||||||
@ -486,7 +490,7 @@ public class ReaderService : IReaderService
|
|||||||
{
|
{
|
||||||
if (volume.Number == currentVolume.Number)
|
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);
|
currentChapter.Range, dto => dto.Range);
|
||||||
if (chapterId > 0) return chapterId;
|
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
|
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 (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
|
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;
|
if (lastChapter == null) return -1;
|
||||||
return lastChapter.Id;
|
return lastChapter.Id;
|
||||||
}
|
}
|
||||||
@ -504,7 +508,7 @@ public class ReaderService : IReaderService
|
|||||||
var lastVolume = volumes.MaxBy(v => v.Number);
|
var lastVolume = volumes.MaxBy(v => v.Number);
|
||||||
if (currentVolume.Number == 0 && currentVolume.Number != lastVolume?.Number && lastVolume?.Chapters.Count > 1)
|
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;
|
if (lastChapter == null) return -1;
|
||||||
return lastChapter.Id;
|
return lastChapter.Id;
|
||||||
}
|
}
|
||||||
@ -527,8 +531,8 @@ public class ReaderService : IReaderService
|
|||||||
if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId))
|
if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId))
|
||||||
{
|
{
|
||||||
// I think i need a way to sort volumes last
|
// I think i need a way to sort volumes last
|
||||||
return volumes.OrderBy(v => double.Parse(v.Number + string.Empty), _chapterSortComparer).First().Chapters
|
return volumes.OrderBy(v => v.Number.ToString(CultureInfo.InvariantCulture).AsDouble(), _chapterSortComparer).First().Chapters
|
||||||
.OrderBy(c => float.Parse(c.Number)).First();
|
.OrderBy(c => c.Number.AsFloat()).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all chapters that are not in volume 0
|
// 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
|
// 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.
|
// If there are any volumes that have progress, return those. If not, move on.
|
||||||
var currentlyReadingChapter = volumeChapters
|
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);
|
.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0);
|
||||||
if (currentlyReadingChapter != null) return currentlyReadingChapter;
|
if (currentlyReadingChapter != null) return currentlyReadingChapter;
|
||||||
|
|
||||||
// Order with volume 0 last so we prefer the natural order
|
// Order with volume 0 last so we prefer the natural order
|
||||||
return FindNextReadingChapter(volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default)
|
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<ChapterDto> volumeChapters)
|
private static ChapterDto FindNextReadingChapter(IList<ChapterDto> volumeChapters)
|
||||||
@ -616,8 +621,8 @@ public class ReaderService : IReaderService
|
|||||||
foreach (var volume in volumes.OrderBy(v => v.Number))
|
foreach (var volume in volumes.OrderBy(v => v.Number))
|
||||||
{
|
{
|
||||||
var chapters = volume.Chapters
|
var chapters = volume.Chapters
|
||||||
.Where(c => !c.IsSpecial && Tasks.Scanner.Parser.Parser.MaxNumberFromRange(c.Range) <= chapterNumber)
|
.Where(c => !c.IsSpecial && Parser.MaxNumberFromRange(c.Range) <= chapterNumber)
|
||||||
.OrderBy(c => float.Parse(c.Number));
|
.OrderBy(c => c.Number.AsFloat());
|
||||||
await MarkChaptersAsRead(user, volume.SeriesId, chapters.ToList());
|
await MarkChaptersAsRead(user, volume.SeriesId, chapters.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using API.DTOs.ReadingLists;
|
|||||||
using API.DTOs.ReadingLists.CBL;
|
using API.DTOs.ReadingLists.CBL;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
@ -390,7 +391,7 @@ public class ReadingListService : IReadingListService
|
|||||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes))
|
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes))
|
||||||
.OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name))
|
.OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name))
|
||||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting)
|
.ThenBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var index = readingList.Items.Count == 0 ? 0 : lastOrder + 1;
|
var index = readingList.Items.Count == 0 ? 0 : lastOrder + 1;
|
||||||
|
@ -13,6 +13,7 @@ using API.DTOs.SeriesDetail;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
@ -77,20 +78,22 @@ public class SeriesService : ISeriesService
|
|||||||
public static Chapter? GetFirstChapterForMetadata(Series series)
|
public static Chapter? GetFirstChapterForMetadata(Series series)
|
||||||
{
|
{
|
||||||
var sortedVolumes = series.Volumes
|
var sortedVolumes = series.Volumes
|
||||||
.Where(v => float.TryParse(v.Name, out var parsedValue) && parsedValue != 0.0f)
|
.Where(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) && parsedValue != 0.0f)
|
||||||
.OrderBy(v => float.TryParse(v.Name, out var parsedValue) ? parsedValue : float.MaxValue);
|
.OrderBy(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) ? parsedValue : float.MaxValue);
|
||||||
var minVolumeNumber = sortedVolumes
|
var minVolumeNumber = sortedVolumes
|
||||||
.MinBy(v => float.Parse(v.Name));
|
.MinBy(v => v.Name.AsFloat());
|
||||||
|
|
||||||
|
|
||||||
var allChapters = series.Volumes
|
var allChapters = series.Volumes
|
||||||
.SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default)).ToList();
|
.SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default))
|
||||||
var minChapter = allChapters
|
.ToList();
|
||||||
|
var minChapter = allChapters
|
||||||
.FirstOrDefault();
|
.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;
|
return minChapter;
|
||||||
@ -436,7 +439,9 @@ public class SeriesService : ISeriesService
|
|||||||
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
||||||
foreach (var volume in volumes)
|
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();
|
var firstChapter = volume.Chapters.First();
|
||||||
// On Books, skip volumes that are specials, since these will be shown
|
// On Books, skip volumes that are specials, since these will be shown
|
||||||
if (firstChapter.IsSpecial) continue;
|
if (firstChapter.IsSpecial) continue;
|
||||||
@ -450,7 +455,7 @@ public class SeriesService : ISeriesService
|
|||||||
processedVolumes.ForEach(v =>
|
processedVolumes.ForEach(v =>
|
||||||
{
|
{
|
||||||
v.Name = $"Volume {v.Name}";
|
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;
|
if (v.Number == 0) return c;
|
||||||
c.VolumeTitle = v.Name;
|
c.VolumeTitle = v.Name;
|
||||||
return c;
|
return c;
|
||||||
}).OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default)).ToList();
|
}).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList();
|
||||||
|
|
||||||
foreach (var chapter in chapters)
|
foreach (var chapter in chapters)
|
||||||
{
|
{
|
||||||
@ -485,12 +490,12 @@ public class SeriesService : ISeriesService
|
|||||||
var storylineChapters = volumes
|
var storylineChapters = volumes
|
||||||
.Where(v => v.Number == 0)
|
.Where(v => v.Number == 0)
|
||||||
.SelectMany(v => v.Chapters.Where(c => !c.IsSpecial))
|
.SelectMany(v => v.Chapters.Where(c => !c.IsSpecial))
|
||||||
.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default)
|
.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// When there's chapters without a volume number revert to chapter sorting only as opposed to volume then chapter
|
// When there's chapters without a volume number revert to chapter sorting only as opposed to volume then chapter
|
||||||
if (storylineChapters.Any()) {
|
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()
|
return new SeriesDetailDto()
|
||||||
@ -717,33 +722,8 @@ public class SeriesService : ISeriesService
|
|||||||
? chapters.Max(c => c.CreatedUtc) + TimeSpan.FromDays(forecastedTimeDifference)
|
? chapters.Max(c => c.CreatedUtc) + TimeSpan.FromDays(forecastedTimeDifference)
|
||||||
: (DateTime?)null;
|
: (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
|
// 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,
|
float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture,
|
||||||
out var lastChapterNumber);
|
out var lastChapterNumber);
|
||||||
|
|
||||||
@ -759,7 +739,7 @@ public class SeriesService : ISeriesService
|
|||||||
|
|
||||||
if (lastChapterNumber > 0)
|
if (lastChapterNumber > 0)
|
||||||
{
|
{
|
||||||
result.ChapterNumber = lastChapterNumber + 1;
|
result.ChapterNumber = (int) Math.Truncate(lastChapterNumber) + 1;
|
||||||
result.VolumeNumber = lastChapter.Volume.Number;
|
result.VolumeNumber = lastChapter.Volume.Number;
|
||||||
result.Title = series.Library.Type switch
|
result.Title = series.Library.Type switch
|
||||||
{
|
{
|
||||||
@ -783,9 +763,9 @@ public class SeriesService : ISeriesService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double ExponentialSmoothing(IEnumerable<double> data, double alpha)
|
private static double ExponentialSmoothing(IList<double> data, double alpha)
|
||||||
{
|
{
|
||||||
double forecast = data.First();
|
var forecast = data.First();
|
||||||
|
|
||||||
foreach (var value in data)
|
foreach (var value in data)
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@ -70,7 +71,10 @@ public class TachiyomiService : ITachiyomiService
|
|||||||
var looseLeafChapterVolume = volumes.Find(v => v.Number == 0);
|
var looseLeafChapterVolume = volumes.Find(v => v.Number == 0);
|
||||||
if (looseLeafChapterVolume == null)
|
if (looseLeafChapterVolume == null)
|
||||||
{
|
{
|
||||||
var volumeChapter = _mapper.Map<ChapterDto>(volumes.Last().Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparerZeroFirst.Default).Last());
|
var volumeChapter = _mapper.Map<ChapterDto>(volumes
|
||||||
|
.Last().Chapters
|
||||||
|
.OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default)
|
||||||
|
.Last());
|
||||||
if (volumeChapter.Number == "0")
|
if (volumeChapter.Number == "0")
|
||||||
{
|
{
|
||||||
var volume = volumes.First(v => v.Id == volumeChapter.VolumeId);
|
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<ChapterDto>(lastChapter);
|
return _mapper.Map<ChapterDto>(lastChapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
_logger.LogError(ex, "There was an error reading an epub file for word count, series skipped");
|
_logger.LogError(ex, "There was an error reading an epub file for word count, series skipped");
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.Error,
|
await _eventHub.SendMessageAsync(MessageFactory.Error,
|
||||||
MessageFactory.ErrorEvent("There was an issue counting words on an epub",
|
MessageFactory.ErrorEvent("There was an issue counting words on an epub",
|
||||||
$"{series.Name} - {file}"));
|
$"{series.Name} - {file.FilePath}"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,9 +279,23 @@ public class ParseScannedFiles
|
|||||||
IEnumerable<string> folders, string libraryName, bool isLibraryScan,
|
IEnumerable<string> folders, string libraryName, bool isLibraryScan,
|
||||||
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<Tuple<bool, IList<ParserInfo>>, Task>? processSeriesInfos, bool forceCheck = false)
|
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<Tuple<bool, IList<ParserInfo>>, Task>? processSeriesInfos, bool forceCheck = false)
|
||||||
{
|
{
|
||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", libraryName, ProgressEventType.Started));
|
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<string> files, string folder)
|
async Task ProcessFolder(IList<string> files, string folder)
|
||||||
{
|
{
|
||||||
var normalizedFolder = Parser.Parser.NormalizePath(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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -38,7 +38,7 @@ public class DefaultParser : IDefaultParser
|
|||||||
|
|
||||||
ParserInfo ret;
|
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
|
ret = new ParserInfo
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
|
|
||||||
namespace API.Services.Tasks.Scanner.Parser;
|
namespace API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
||||||
@ -927,7 +928,7 @@ public static class Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tokens = range.Replace("_", string.Empty).Split("-");
|
var tokens = range.Replace("_", string.Empty).Split("-");
|
||||||
return tokens.Min(float.Parse);
|
return tokens.Min(t => t.AsFloat());
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -945,7 +946,7 @@ public static class Parser
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tokens = range.Replace("_", string.Empty).Split("-");
|
var tokens = range.Replace("_", string.Empty).Split("-");
|
||||||
return tokens.Max(float.Parse);
|
return tokens.Max(t => t.AsFloat());
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
@ -21,6 +22,8 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace API.Services.Tasks.Scanner;
|
namespace API.Services.Tasks.Scanner;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public interface IProcessSeries
|
public interface IProcessSeries
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -208,7 +211,7 @@ public class ProcessSeries : IProcessSeries
|
|||||||
.ToList()}));
|
.ToList()}));
|
||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.Error,
|
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));
|
ex.Message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -614,7 +617,7 @@ public class ProcessSeries : IProcessSeries
|
|||||||
// Add files
|
// Add files
|
||||||
var specialTreatment = info.IsSpecialInfo();
|
var specialTreatment = info.IsSpecialInfo();
|
||||||
AddOrUpdateFileForChapter(chapter, info, forceUpdate);
|
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;
|
chapter.Range = specialTreatment ? info.Filename : info.Chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -886,7 +889,7 @@ public class ProcessSeries : IProcessSeries
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
action(genre, newTag);
|
action(genre!, newTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,27 +240,6 @@ public class ScannerService : IScannerService
|
|||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Started, series.Name));
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Started, series.Name));
|
||||||
|
|
||||||
await _processSeries.Prime();
|
await _processSeries.Prime();
|
||||||
async Task TrackFiles(Tuple<bool, IList<ParserInfo>> 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);
|
_logger.LogInformation("Beginning file scan on {SeriesName}", series.Name);
|
||||||
var scanElapsedTime = await ScanFiles(library, new []{ folderPath }, false, TrackFiles, true);
|
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(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, false));
|
||||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||||
BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory));
|
BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory));
|
||||||
|
return;
|
||||||
|
|
||||||
|
async Task TrackFiles(Tuple<bool, IList<ParserInfo>> 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<ScanCancelReason> ShouldScanSeries(int seriesId, Library library, IList<string> libraryPaths, Series series, bool bypassFolderChecks = false)
|
private async Task<ScanCancelReason> ShouldScanSeries(int seriesId, Library library, IList<string> libraryPaths, Series series, bool bypassFolderChecks = false)
|
||||||
@ -488,38 +490,6 @@ public class ScannerService : IScannerService
|
|||||||
await _processSeries.Prime();
|
await _processSeries.Prime();
|
||||||
var processTasks = new List<Func<Task>>();
|
var processTasks = new List<Func<Task>>();
|
||||||
|
|
||||||
Task TrackFiles(Tuple<bool, IList<ParserInfo>> 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);
|
var scanElapsedTime = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, TrackFiles, forceUpdate);
|
||||||
|
|
||||||
// NOTE: This runs sync after every file is scanned
|
// NOTE: This runs sync after every file is scanned
|
||||||
@ -592,6 +562,39 @@ public class ScannerService : IScannerService
|
|||||||
await _metadataService.RemoveAbandonedMetadataKeys();
|
await _metadataService.RemoveAbandonedMetadataKeys();
|
||||||
|
|
||||||
BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory));
|
BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory));
|
||||||
|
return;
|
||||||
|
|
||||||
|
Task TrackFiles(Tuple<bool, IList<ParserInfo>> 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<long> ScanFiles(Library library, IEnumerable<string> dirs,
|
private async Task<long> ScanFiles(Library library, IEnumerable<string> dirs,
|
||||||
|
21
UI/Web/package-lock.json
generated
21
UI/Web/package-lock.json
generated
@ -998,6 +998,7 @@
|
|||||||
"version": "16.2.9",
|
"version": "16.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.9.tgz",
|
||||||
"integrity": "sha512-ecH2oOlijJdDqioD9IfgdqJGoRRHI6hAx5rwBxIaYk01ywj13KzvXWPrXbCIupeWtV/XUZUlbwf47nlmL5gxZg==",
|
"integrity": "sha512-ecH2oOlijJdDqioD9IfgdqJGoRRHI6hAx5rwBxIaYk01ywj13KzvXWPrXbCIupeWtV/XUZUlbwf47nlmL5gxZg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "7.22.5",
|
"@babel/core": "7.22.5",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||||
@ -5897,6 +5898,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"picomatch": "^2.0.4"
|
"picomatch": "^2.0.4"
|
||||||
@ -6152,6 +6154,7 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -6459,6 +6462,7 @@
|
|||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -7638,6 +7642,7 @@
|
|||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iconv-lite": "^0.6.2"
|
"iconv-lite": "^0.6.2"
|
||||||
@ -7647,6 +7652,7 @@
|
|||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
@ -8743,6 +8749,7 @@
|
|||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -9566,6 +9573,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"binary-extensions": "^2.0.0"
|
"binary-extensions": "^2.0.0"
|
||||||
},
|
},
|
||||||
@ -11404,6 +11412,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -12744,6 +12753,7 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
},
|
},
|
||||||
@ -12754,7 +12764,8 @@
|
|||||||
"node_modules/reflect-metadata": {
|
"node_modules/reflect-metadata": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
||||||
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
|
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/regenerate": {
|
"node_modules/regenerate": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
@ -13190,7 +13201,7 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"devOptional": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.64.1",
|
"version": "1.64.1",
|
||||||
@ -13317,6 +13328,7 @@
|
|||||||
"version": "7.5.3",
|
"version": "7.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
|
||||||
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
|
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
},
|
},
|
||||||
@ -13331,6 +13343,7 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
},
|
},
|
||||||
@ -13341,7 +13354,8 @@
|
|||||||
"node_modules/semver/node_modules/yallist": {
|
"node_modules/semver/node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
@ -14457,6 +14471,7 @@
|
|||||||
"version": "5.1.6",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
|
||||||
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
|
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
|
||||||
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CanActivate } from '@angular/router';
|
import {CanActivate, Router} from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map, take } from 'rxjs/operators';
|
||||||
@ -11,10 +11,10 @@ import {TranslocoService} from "@ngneat/transloco";
|
|||||||
})
|
})
|
||||||
export class AdminGuard implements CanActivate {
|
export class AdminGuard implements CanActivate {
|
||||||
constructor(private accountService: AccountService, private toastr: ToastrService,
|
constructor(private accountService: AccountService, private toastr: ToastrService,
|
||||||
|
private router: Router,
|
||||||
private translocoService: TranslocoService) {}
|
private translocoService: TranslocoService) {}
|
||||||
|
|
||||||
canActivate(): Observable<boolean> {
|
canActivate(): Observable<boolean> {
|
||||||
// this automatically subs due to being router guard
|
|
||||||
return this.accountService.currentUser$.pipe(take(1),
|
return this.accountService.currentUser$.pipe(take(1),
|
||||||
map((user) => {
|
map((user) => {
|
||||||
if (user && this.accountService.hasAdminRole(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.toastr.error(this.translocoService.translate('toasts.unauthorized-1'));
|
||||||
|
this.router.navigateByUrl('/libraries');
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -22,12 +22,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
if (user) {
|
if (user) {
|
||||||
return true;
|
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);
|
localStorage.setItem(this.urlKey, window.location.pathname);
|
||||||
this.router.navigateByUrl('/login');
|
this.router.navigateByUrl('/login');
|
||||||
return false;
|
return false;
|
||||||
|
@ -124,9 +124,9 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="tags" class="form-label">{{t('tags-label')}}</label>
|
<label for="tags" class="form-label">{{t('tags-label')}}</label>
|
||||||
<app-typeahead (selectedData)="updateTags($event)" [settings]="tagsSettings"
|
<app-typeahead (selectedData)="updateTags($event);metadata.tagsLocked = true" [settings]="tagsSettings"
|
||||||
[(locked)]="metadata.tagsLocked" (onUnlock)="metadata.tagsLocked = false"
|
[(locked)]="metadata.tagsLocked" (onUnlock)="metadata.tagsLocked = false"
|
||||||
(newItemAdded)="metadata.tagsLocked = true" (selectedData)="metadata.tagsLocked = true">
|
(newItemAdded)="metadata.tagsLocked = true">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -368,7 +368,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||||||
return {id: 0, title: title };
|
return {id: 0, title: title };
|
||||||
});
|
});
|
||||||
this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => {
|
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) => {
|
this.tagsSettings.compareFnForAdd = (options: Tag[], filter: string) => {
|
||||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
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));
|
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||||
}
|
}
|
||||||
this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => {
|
this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => {
|
||||||
return a.title == b.title;
|
return a.title.toLowerCase() == b.title.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.genreSettings.addTransformFn = ((title: string) => {
|
this.genreSettings.addTransformFn = ((title: string) => {
|
||||||
|
@ -103,11 +103,10 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
const dataURL = canvas.toDataURL("image/png");
|
return canvas.toDataURL("image/png");
|
||||||
return dataURL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectImage(index: number) {
|
selectImage(index: number, callback?: Function) {
|
||||||
if (this.selectedIndex === index) { return; }
|
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
|
// 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();
|
const img = new Image();
|
||||||
img.crossOrigin = 'Anonymous';
|
img.crossOrigin = 'Anonymous';
|
||||||
img.src = imgUrl;
|
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) => {
|
img.onerror = (e) => {
|
||||||
this.toastr.error(translate('errors.rejected-cover-upload'));
|
this.toastr.error(translate('errors.rejected-cover-upload'));
|
||||||
this.form.get('coverImageUrl')?.setValue('');
|
this.form.get('coverImageUrl')?.setValue('');
|
||||||
@ -124,7 +127,6 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
this.form.get('coverImageUrl')?.setValue('');
|
this.form.get('coverImageUrl')?.setValue('');
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
this.selectedBase64Url.emit(this.imageUrls[this.selectedIndex]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,11 +137,13 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyImage(index: number) {
|
applyImage(index: number) {
|
||||||
if (this.showApplyButton) {
|
if (!this.showApplyButton) return;
|
||||||
|
|
||||||
|
this.selectImage(index, () => {
|
||||||
this.applyCover.emit(this.imageUrls[index]);
|
this.applyCover.emit(this.imageUrls[index]);
|
||||||
this.appliedIndex = index;
|
this.appliedIndex = index;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resetImage() {
|
resetImage() {
|
||||||
|
@ -1645,6 +1645,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
data.emulateBook = modelSettings.emulateBook;
|
data.emulateBook = modelSettings.emulateBook;
|
||||||
data.swipeToPaginate = modelSettings.swipeToPaginate;
|
data.swipeToPaginate = modelSettings.swipeToPaginate;
|
||||||
data.pageSplitOption = parseInt(modelSettings.pageSplitOption, 10);
|
data.pageSplitOption = parseInt(modelSettings.pageSplitOption, 10);
|
||||||
|
data.locale = data.locale || 'en';
|
||||||
|
|
||||||
this.accountService.updatePreferences(data).subscribe(updatedPrefs => {
|
this.accountService.updatePreferences(data).subscribe(updatedPrefs => {
|
||||||
this.toastr.success(translate('manga-reader.user-preferences-updated'));
|
this.toastr.success(translate('manga-reader.user-preferences-updated'));
|
||||||
|
@ -53,7 +53,6 @@ export class UserLoginComponent implements OnInit {
|
|||||||
if (user) {
|
if (user) {
|
||||||
this.navService.showSideNav();
|
this.navService.showSideNav();
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
this.router.navigateByUrl('/libraries');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,6 +95,7 @@ export class UserLoginComponent implements OnInit {
|
|||||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||||
this.router.navigateByUrl(pageResume);
|
this.router.navigateByUrl(pageResume);
|
||||||
} else {
|
} else {
|
||||||
|
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||||
this.router.navigateByUrl('/libraries');
|
this.router.navigateByUrl('/libraries');
|
||||||
}
|
}
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
|
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
|
||||||
<ng-template #itemTemplate let-item>
|
<ng-template #itemTemplate let-item>
|
||||||
<a class="col me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item">
|
<a class="col me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item">
|
||||||
<img width="24" height="24" class="lazyload img-placeholder"
|
<img width="24" height="24" class="lazyload img-placeholder favicon"
|
||||||
[src]="imageService.errorWebLinkImage"
|
[src]="imageService.errorWebLinkImage"
|
||||||
[attr.data-src]="imageService.getWebLinkImage(item)"
|
[attr.data-src]="imageService.getWebLinkImage(item)"
|
||||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.favicon {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
@ -201,7 +201,7 @@ export class FilterUtilitiesService {
|
|||||||
|
|
||||||
if (sortFieldPart && isAscendingPart) {
|
if (sortFieldPart && isAscendingPart) {
|
||||||
const sortField = parseInt(sortFieldPart.split('=')[1], 10) as SortField;
|
const sortField = parseInt(sortFieldPart.split('=')[1], 10) as SortField;
|
||||||
const isAscending = isAscendingPart.split('=')[1] === 'true';
|
const isAscending = isAscendingPart.split('=')[1].toLowerCase() === 'true';
|
||||||
return {sortField, isAscending};
|
return {sortField, isAscending};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.9.2"
|
"version": "0.7.9.3"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user