diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index daf3b461e..03e5f2ec7 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -10,9 +10,9 @@ - - - + + + diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 24d6cb94c..652cec7c9 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -6,13 +6,13 @@ - - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index 5e3e1cef8..cc1811292 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -82,6 +82,7 @@ public class MangaParserTests [InlineData("63권#200", "63")] [InlineData("시즌34삽화2", "34")] [InlineData("Accel World Chapter 001 Volume 002", "2")] + [InlineData("Accel World Volume 2", "2")] public void ParseVolumeTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename)); @@ -198,6 +199,7 @@ public class MangaParserTests [InlineData("Accel World: Vol 1", "Accel World")] [InlineData("Accel World Chapter 001 Volume 002", "Accel World")] [InlineData("Bleach 001-003", "Bleach")] + [InlineData("Accel World Volume 2", "Accel World")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename)); @@ -283,6 +285,7 @@ public class MangaParserTests [InlineData("Манга Том 1 2 Глава", "2")] [InlineData("Accel World Chapter 001 Volume 002", "1")] [InlineData("Bleach 001-003", "1-3")] + [InlineData("Accel World Volume 2", "0")] public void ParseChaptersTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename)); diff --git a/API.Tests/Repository/SeriesRepositoryTests.cs b/API.Tests/Repository/SeriesRepositoryTests.cs index b24e53d7f..824e749b5 100644 --- a/API.Tests/Repository/SeriesRepositoryTests.cs +++ b/API.Tests/Repository/SeriesRepositoryTests.cs @@ -122,6 +122,10 @@ public class SeriesRepositoryTests .WithLocalizedName("Heion Sedai no Idaten-tachi") .WithFormat(MangaFormat.Archive) .Build()) + .WithSeries(new SeriesBuilder("Hitomi-chan is Shy With Strangers") + .WithLocalizedName("Hitomi-chan wa Hitomishiri") + .WithFormat(MangaFormat.Archive) + .Build()) .Build(); _unitOfWork.LibraryRepository.Add(library); @@ -133,6 +137,7 @@ public class SeriesRepositoryTests [InlineData("The Idaten Deities Know Only Peace", MangaFormat.Archive, "", "The Idaten Deities Know Only Peace")] // Matching on series name in DB [InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Archive, "The Idaten Deities Know Only Peace", "The Idaten Deities Know Only Peace")] // Matching on localized name in DB [InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Pdf, "", null)] + [InlineData("Hitomi-chan wa Hitomishiri", MangaFormat.Archive, "", "Hitomi-chan is Shy With Strangers")] public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected) { await ResetDb(); diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 4a2ed0f32..b93a6fe83 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -1134,10 +1134,10 @@ public class SeriesServiceTests : AbstractDbTest await _seriesService.UpdateRelatedSeries(addRelationDto); - Assert.Empty(_seriesService.GetRelatedSeries(1, 2).Result.Parent); - Assert.Empty(_seriesService.GetRelatedSeries(1, 3).Result.Parent); - Assert.Empty(_seriesService.GetRelatedSeries(1, 4).Result.Parent); - Assert.NotEmpty(_seriesService.GetRelatedSeries(1, 5).Result.Parent); + Assert.Empty((await _seriesService.GetRelatedSeries(1, 2)).Parent); + Assert.Empty((await _seriesService.GetRelatedSeries(1, 3)).Parent); + Assert.Empty((await _seriesService.GetRelatedSeries(1, 4)).Parent); + Assert.NotEmpty((await _seriesService.GetRelatedSeries(1, 5)).Parent); } [Fact] diff --git a/API/API.csproj b/API/API.csproj index 829f3fde9..a72699245 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -55,54 +55,54 @@ - - - + + + - - + + - + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index e1742a519..8e2df09fe 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -193,7 +193,11 @@ public class AccountController : BaseApiController } - if (user == null) return Unauthorized(await _localizationService.Get("en", "bad-credentials")); + if (user == null) + { + _logger.LogWarning("Attempted login by {UserName} failed due to unable to find account", loginDto.Username); + return Unauthorized(await _localizationService.Get("en", "bad-credentials")); + } var roles = await _userManager.GetRolesAsync(user); if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account")); @@ -205,12 +209,19 @@ public class AccountController : BaseApiController if (result.IsLockedOut) { await _userManager.UpdateSecurityStampAsync(user); - return Unauthorized(await _localizationService.Translate(user.Id, "locked-out")); + var errorStr = await _localizationService.Translate(user.Id, "locked-out"); + _logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, + errorStr); + return Unauthorized(errorStr); } if (!result.Succeeded) { - return Unauthorized(await _localizationService.Translate(user.Id, result.IsNotAllowed ? "confirm-email" : "bad-credentials")); + var errorStr = await _localizationService.Translate(user.Id, + result.IsNotAllowed ? "confirm-email" : "bad-credentials"); + _logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, + errorStr); + return Unauthorized(errorStr); } } @@ -399,7 +410,6 @@ public class AccountController : BaseApiController _logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); } - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(); diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 8a98b1dbe..2122de616 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -51,7 +51,6 @@ public interface ILibraryRepository Task DoAnySeriesFoldersMatch(IEnumerable folders); Task GetLibraryCoverImageAsync(int libraryId); Task> GetAllCoverImagesAsync(); - Task> GetLibraryTypesForIdsAsync(IEnumerable libraryIds); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task GetAllowsScrobblingBySeriesId(int seriesId); } @@ -346,28 +345,6 @@ public class LibraryRepository : ILibraryRepository .ToListAsync())!; } - public async Task> GetLibraryTypesForIdsAsync(IEnumerable libraryIds) - { - var types = await _context.Library - .Where(l => libraryIds.Contains(l.Id)) - .AsNoTracking() - .Select(l => new - { - LibraryId = l.Id, - LibraryType = l.Type - }) - .ToListAsync(); - - var dict = new Dictionary(); - - foreach (var type in types) - { - dict.TryAdd(type.LibraryId, type.LibraryType); - } - - return dict; - } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) { var extension = encodeFormat.GetExtension(); diff --git a/API/Data/Repositories/MangaFileRepository.cs b/API/Data/Repositories/MangaFileRepository.cs index ddea0d51a..debd52199 100644 --- a/API/Data/Repositories/MangaFileRepository.cs +++ b/API/Data/Repositories/MangaFileRepository.cs @@ -9,7 +9,6 @@ namespace API.Data.Repositories; public interface IMangaFileRepository { void Update(MangaFile file); - Task AnyMissingExtension(); Task> GetAllWithMissingExtension(); } @@ -27,11 +26,6 @@ public class MangaFileRepository : IMangaFileRepository _context.Entry(file).State = EntityState.Modified; } - public async Task AnyMissingExtension() - { - return (await _context.MangaFile.CountAsync(f => string.IsNullOrEmpty(f.Extension))) > 0; - } - public async Task> GetAllWithMissingExtension() { return await _context.MangaFile diff --git a/API/Data/Repositories/MediaErrorRepository.cs b/API/Data/Repositories/MediaErrorRepository.cs index e9062d285..c2e932d32 100644 --- a/API/Data/Repositories/MediaErrorRepository.cs +++ b/API/Data/Repositories/MediaErrorRepository.cs @@ -15,7 +15,6 @@ public interface IMediaErrorRepository void Attach(MediaError error); void Remove(MediaError error); Task Find(string filename); - Task> GetAllErrorDtosAsync(UserParams userParams); IEnumerable GetAllErrorDtosAsync(); Task ExistsAsync(MediaError error); Task DeleteAll(); @@ -49,15 +48,6 @@ public class MediaErrorRepository : IMediaErrorRepository return _context.MediaError.Where(e => e.FilePath == filename).SingleOrDefaultAsync(); } - public Task> GetAllErrorDtosAsync(UserParams userParams) - { - var query = _context.MediaError - .OrderByDescending(m => m.Created) - .ProjectTo(_mapper.ConfigurationProvider) - .AsNoTracking(); - return PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); - } - public IEnumerable GetAllErrorDtosAsync() { var query = _context.MediaError diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index a6329f887..146479740 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -19,7 +19,7 @@ public interface IPersonRepository Task> GetAllPeople(); Task> GetAllPersonDtosAsync(int userId); Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role); - Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false); + Task RemoveAllPeopleNoLongerAssociated(); Task> GetAllPeopleDtosForLibrariesAsync(List libraryIds, int userId); Task GetCountAsync(); @@ -46,7 +46,7 @@ public class PersonRepository : IPersonRepository _context.Person.Remove(person); } - public async Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false) + public async Task RemoveAllPeopleNoLongerAssociated() { var peopleWithNoConnections = await _context.Person .Include(p => p.SeriesMetadatas) diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs index 62a793479..bff82c681 100644 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ b/API/Data/Repositories/ScrobbleEventRepository.cs @@ -18,7 +18,7 @@ public interface IScrobbleRepository void Attach(ScrobbleEvent evt); void Attach(ScrobbleError error); void Remove(ScrobbleEvent evt); - void Remove(IList evts); + void Remove(IEnumerable events); void Update(ScrobbleEvent evt); Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false); Task> GetProcessedEvents(int daysAgo); @@ -60,9 +60,9 @@ public class ScrobbleRepository : IScrobbleRepository _context.ScrobbleEvent.Remove(evt); } - public void Remove(IList evts) + public void Remove(IEnumerable events) { - _context.ScrobbleEvent.RemoveRange(evts); + _context.ScrobbleEvent.RemoveRange(events); } public void Update(ScrobbleEvent evt) diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index d6ad22931..03b7dd38e 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -128,8 +128,6 @@ public interface ISeriesRepository Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None); Task> GetAllSeriesByNameAsync(IList normalizedNames, int userId, SeriesIncludes includes = SeriesIncludes.None); - Task> GetAllSeriesDtosByNameAsync(IEnumerable normalizedNames, - int userId, SeriesIncludes includes = SeriesIncludes.None); Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true); Task> RemoveSeriesNotInList(IList seenSeries, int libraryId); Task>> GetFolderPathMap(int libraryId); @@ -1054,7 +1052,7 @@ public class SeriesRepository : ISeriesRepository private static IQueryable BuildFilterGroup(int userId, FilterStatementDto statement, IQueryable query) { - var (value, _) = FilterFieldValueConverter.ConvertValue(statement.Field, statement.Value); + var value = FilterFieldValueConverter.ConvertValue(statement.Field, statement.Value); return statement.Field switch { FilterField.Summary => query.HasSummary(true, statement.Comparison, (string) value), @@ -1085,7 +1083,7 @@ public class SeriesRepository : ISeriesRepository FilterField.WantToRead => // This is handled in the higher level of code as it's more general query, - FilterField.ReadProgress => query.HasReadingProgress(true, statement.Comparison, (int) value, userId), + FilterField.ReadProgress => query.HasReadingProgress(true, statement.Comparison, (float) value, userId), FilterField.Formats => query.HasFormat(true, statement.Comparison, (IList) value), FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value), FilterField.ReadTime => query.HasAverageReadTime(true, statement.Comparison, (int) value), @@ -1471,20 +1469,6 @@ public class SeriesRepository : ISeriesRepository .ToListAsync(); } - public async Task> GetAllSeriesDtosByNameAsync(IEnumerable normalizedNames, int userId, - SeriesIncludes includes = SeriesIncludes.None) - { - var libraryIds = _context.Library.GetUserLibraries(userId); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - - return await _context.Series - .Where(s => normalizedNames.Contains(s.NormalizedName)) - .Where(s => libraryIds.Contains(s.LibraryId)) - .RestrictAgainstAgeRestriction(userRating) - .Includes(includes) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } /// /// Finds a series by series name or localized name for a given library. diff --git a/API/Data/Repositories/SiteThemeRepository.cs b/API/Data/Repositories/SiteThemeRepository.cs index b2c082183..4e1a01c98 100644 --- a/API/Data/Repositories/SiteThemeRepository.cs +++ b/API/Data/Repositories/SiteThemeRepository.cs @@ -19,7 +19,6 @@ public interface ISiteThemeRepository Task GetThemeDtoByName(string themeName); Task GetDefaultTheme(); Task> GetThemes(); - Task GetThemeById(int themeId); } public class SiteThemeRepository : ISiteThemeRepository @@ -89,13 +88,6 @@ public class SiteThemeRepository : ISiteThemeRepository .ToListAsync(); } - public async Task GetThemeById(int themeId) - { - return await _context.SiteTheme - .Where(t => t.Id == themeId) - .SingleOrDefaultAsync(); - } - public async Task GetThemeDto(int themeId) { return await _context.SiteTheme diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index c8d58a2bf..7544694ea 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -17,7 +17,7 @@ public interface ITagRepository void Remove(Tag tag); Task> GetAllTagsAsync(); Task> GetAllTagDtosAsync(int userId); - Task RemoveAllTagNoLongerAssociated(bool removeExternal = false); + Task RemoveAllTagNoLongerAssociated(); Task> GetAllTagDtosForLibrariesAsync(IList libraryIds, int userId); } @@ -42,7 +42,7 @@ public class TagRepository : ITagRepository _context.Tag.Remove(tag); } - public async Task RemoveAllTagNoLongerAssociated(bool removeExternal = false) + public async Task RemoveAllTagNoLongerAssociated() { var tagsWithNoConnections = await _context.Tag .Include(p => p.SeriesMetadatas) diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index 3bc97b3ad..6b1472d6f 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -233,7 +233,7 @@ public static class SeriesFilter /// /// public static IQueryable HasReadingProgress(this IQueryable queryable, bool condition, - FilterComparison comparison, int readProgress, int userId) + FilterComparison comparison, float readProgress, int userId) { if (!condition) return queryable; diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/API/Helpers/Converters/FilterFieldValueConverter.cs index 4bc057835..a09af509e 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/API/Helpers/Converters/FilterFieldValueConverter.cs @@ -8,72 +8,72 @@ namespace API.Helpers.Converters; public static class FilterFieldValueConverter { - public static (object Value, Type Type) ConvertValue(FilterField field, string value) + public static object ConvertValue(FilterField field, string value) { return field switch { - FilterField.SeriesName => (value, typeof(string)), - FilterField.Path => (value, typeof(string)), - FilterField.FilePath => (value, typeof(string)), - FilterField.ReleaseYear => (int.Parse(value), typeof(int)), - FilterField.Languages => (value.Split(',').ToList(), typeof(IList)), - FilterField.PublicationStatus => (value.Split(',') + FilterField.SeriesName => value, + FilterField.Path => value, + FilterField.FilePath => value, + FilterField.ReleaseYear => int.Parse(value), + FilterField.Languages => value.Split(',').ToList(), + FilterField.PublicationStatus => value.Split(',') .Select(x => (PublicationStatus) Enum.Parse(typeof(PublicationStatus), x)) - .ToList(), typeof(IList)), - FilterField.Summary => (value, typeof(string)), - FilterField.AgeRating => (value.Split(',') + .ToList(), + FilterField.Summary => value, + FilterField.AgeRating => value.Split(',') .Select(x => (AgeRating) Enum.Parse(typeof(AgeRating), x)) - .ToList(), typeof(IList)), - FilterField.UserRating => (int.Parse(value), typeof(int)), - FilterField.Tags => (value.Split(',') + .ToList(), + FilterField.UserRating => int.Parse(value), + FilterField.Tags => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.CollectionTags => (value.Split(',') + .ToList(), + FilterField.CollectionTags => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Translators => (value.Split(',') + .ToList(), + FilterField.Translators => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Characters => (value.Split(',') + .ToList(), + FilterField.Characters => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Publisher => (value.Split(',') + .ToList(), + FilterField.Publisher => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Editor => (value.Split(',') + .ToList(), + FilterField.Editor => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.CoverArtist => (value.Split(',') + .ToList(), + FilterField.CoverArtist => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Letterer => (value.Split(',') + .ToList(), + FilterField.Letterer => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Colorist => (value.Split(',') + .ToList(), + FilterField.Colorist => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Inker => (value.Split(',') + .ToList(), + FilterField.Inker => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Penciller => (value.Split(',') + .ToList(), + FilterField.Penciller => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Writers => (value.Split(',') + .ToList(), + FilterField.Writers => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Genres => (value.Split(',') + .ToList(), + FilterField.Genres => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.Libraries => (value.Split(',') + .ToList(), + FilterField.Libraries => value.Split(',') .Select(int.Parse) - .ToList(), typeof(IList)), - FilterField.WantToRead => (bool.Parse(value), typeof(bool)), - FilterField.ReadProgress => (int.Parse(value), typeof(int)), - FilterField.ReadingDate => (DateTime.Parse(value), typeof(DateTime?)), - FilterField.Formats => (value.Split(',') + .ToList(), + FilterField.WantToRead => bool.Parse(value), + FilterField.ReadProgress => float.Parse(value), + FilterField.ReadingDate => DateTime.Parse(value), + FilterField.Formats => value.Split(',') .Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x)) - .ToList(), typeof(IList)), - FilterField.ReadTime => (int.Parse(value), typeof(int)), + .ToList(), + FilterField.ReadTime => int.Parse(value), _ => throw new ArgumentException("Invalid field type") }; } diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 280f1cbb4..9b01f2eb3 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -85,7 +85,7 @@ public class DirectoryService : IDirectoryService private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; private static readonly Regex ExcludeDirectories = new Regex( - @"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash", + @"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash|#recycle", MatchOptions, Tasks.Scanner.Parser.Parser.RegexTimeout); private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)", diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index ea95de1fd..62c882901 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -550,9 +550,10 @@ public static class Parser new Regex( @"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?\d+(?:.\d+|-\d+)?)", MatchOptions, RegexTimeout), + // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz new Regex( - @"^(?!Vol)(?.+?)(?\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", + @"^(?.+?)(?\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", MatchOptions, RegexTimeout), // Tower Of God S01 014 (CBT) (digital).cbz new Regex( @@ -997,6 +998,7 @@ public static class Parser { return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle") || path.StartsWith("._") || Path.GetFileName(path).StartsWith("._") || path.Contains(".qpkg") + || path.StartsWith("#recycle") || path.Contains(".caltrash"); } diff --git a/API/SignalR/EventHub.cs b/API/SignalR/EventHub.cs index 3f5eed44a..fcdc17b14 100644 --- a/API/SignalR/EventHub.cs +++ b/API/SignalR/EventHub.cs @@ -1,5 +1,6 @@ using System.Linq; using System; +using System.Collections.Generic; using System.Threading.Tasks; using API.Data; using API.SignalR.Presence; @@ -55,8 +56,7 @@ public class EventHub : IEventHub /// public async Task SendMessageToAsync(string method, SignalRMessage message, int userId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId) ?? throw new InvalidOperationException(); - await _messageHub.Clients.User(user.UserName!).SendAsync(method, message); + await _messageHub.Clients.Users(new List() {userId + string.Empty}).SendAsync(method, message); } } diff --git a/API/SignalR/MessageHub.cs b/API/SignalR/MessageHub.cs index 1e75e13de..85c1467fb 100644 --- a/API/SignalR/MessageHub.cs +++ b/API/SignalR/MessageHub.cs @@ -22,7 +22,8 @@ public class MessageHub : Hub public override async Task OnConnectedAsync() { - await _tracker.UserConnected(Context.User!.GetUserId(), Context.ConnectionId); + var userId = Context.User!.GetUserId(); + await _tracker.UserConnected(userId, Context.ConnectionId); var currentUsers = await PresenceTracker.GetOnlineUsers(); await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers); diff --git a/API/SignalR/Presence/PresenceTracker.cs b/API/SignalR/Presence/PresenceTracker.cs index fc4970a52..87d748841 100644 --- a/API/SignalR/Presence/PresenceTracker.cs +++ b/API/SignalR/Presence/PresenceTracker.cs @@ -80,7 +80,10 @@ public class PresenceTracker : IPresenceTracker string[] onlineUsers; lock (OnlineUsers) { - onlineUsers = OnlineUsers.OrderBy(k => k.Value.UserName).Select(k => k.Value.UserName).ToArray(); + onlineUsers = OnlineUsers + .Select(k => k.Value.UserName) + .Order() + .ToArray(); } return Task.FromResult(onlineUsers); @@ -91,7 +94,10 @@ public class PresenceTracker : IPresenceTracker int[] onlineUsers; lock (OnlineUsers) { - onlineUsers = OnlineUsers.Where(pair => pair.Value.IsAdmin).OrderBy(k => k.Key).Select(k => k.Key).ToArray(); + onlineUsers = OnlineUsers.Where(pair => pair.Value.IsAdmin) + .Select(k => k.Key) + .Order() + .ToArray(); } diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 124a45e4e..a98c7c878 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index cae5ef64e..8de9cee98 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -20,7 +20,7 @@ "@angular/router": "^16.1.8", "@fortawesome/fontawesome-free": "^6.4.2", "@iharbeck/ngx-virtual-scroller": "^16.0.0", - "@iplab/ngx-file-upload": "^16.0.1", + "@iplab/ngx-file-upload": "^16.0.2", "@microsoft/signalr": "^7.0.11", "@ng-bootstrap/ng-bootstrap": "^15.1.1", "@ngneat/transloco": "^5.0.7", @@ -37,8 +37,8 @@ "file-saver": "^2.0.5", "lazysizes": "^5.3.2", "ng-circle-progress": "^1.7.1", - "ng-select2-component": "^13.0.6", - "ngx-color-picker": "^14.0.0", + "ng-select2-component": "^13.0.9", + "ngx-color-picker": "^15.0.0", "ngx-extended-pdf-viewer": "^16.2.16", "ngx-file-drop": "^16.0.0", "ngx-slider-v2": "^16.0.2", @@ -2950,9 +2950,9 @@ } }, "node_modules/@iplab/ngx-file-upload": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@iplab/ngx-file-upload/-/ngx-file-upload-16.0.1.tgz", - "integrity": "sha512-gHnEofzmAv9x1YQzjwBTcUPriaiX+S+m/v24lWrC+x4FmEpeqxwnsSE0lSlE04owBQpp7cFOFGCvwQ2JjxenhQ==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@iplab/ngx-file-upload/-/ngx-file-upload-16.0.2.tgz", + "integrity": "sha512-6UppO6lROAbkGs+rFZ6ngutsCPrjs/BMnXsIE9UL4AL1NLRRAXrb28pkU2U7KjtDg/0naJ6JFmpRyUBCw5SXhg==", "dependencies": { "tslib": "^2.3.0" }, @@ -2960,6 +2960,7 @@ "@angular/animations": "^16.0.0", "@angular/common": "^16.0.0", "@angular/core": "^16.0.0", + "@angular/forms": "^16.0.0", "rxjs": "^7.0.0" } }, @@ -10558,9 +10559,9 @@ } }, "node_modules/ng-select2-component": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.6.tgz", - "integrity": "sha512-CiAelglSz2aeYy0BiXRi32zc49Mq27+J1eDzTrXmf2o50MvNo3asS3NRVQcnSldo/zLcJafWCMueVfjVaV1etw==", + "version": "13.0.9", + "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.9.tgz", + "integrity": "sha512-Bj7lHCiHnwNFECyzpn0LyD3IOPnBbIHHYXxpFU313QZgVkEz7oiF9nBnkorAAABIfLk4EiU0nBQkY3CmbVOgfg==", "dependencies": { "ngx-infinite-scroll": ">=16.0.0", "tslib": "^2.3.0" @@ -10572,9 +10573,9 @@ } }, "node_modules/ngx-color-picker": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-14.0.0.tgz", - "integrity": "sha512-w28zx2DyVpIJeNsTB3T2LUI4Ed/Ujf5Uhxuh0dllputfpxXwZG9ocSJM/0L67+fxA3UnfvvXVZNUX1Ny5nZIIw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-15.0.0.tgz", + "integrity": "sha512-+7wK8Pz9pm7ywJQOWELRcLYO9J0q4giF4b5QFxq8J3kEcHsUBn0hKOpBbGud+UmNnOwbJVgU2rhyRpGIDUCDJw==", "dependencies": { "tslib": "^2.3.0" }, diff --git a/UI/Web/package.json b/UI/Web/package.json index d59335084..f1d568b2e 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -25,7 +25,7 @@ "@angular/router": "^16.1.8", "@fortawesome/fontawesome-free": "^6.4.2", "@iharbeck/ngx-virtual-scroller": "^16.0.0", - "@iplab/ngx-file-upload": "^16.0.1", + "@iplab/ngx-file-upload": "^16.0.2", "@microsoft/signalr": "^7.0.11", "@ng-bootstrap/ng-bootstrap": "^15.1.1", "@ngneat/transloco": "^5.0.7", @@ -42,8 +42,8 @@ "file-saver": "^2.0.5", "lazysizes": "^5.3.2", "ng-circle-progress": "^1.7.1", - "ng-select2-component": "^13.0.6", - "ngx-color-picker": "^14.0.0", + "ng-select2-component": "^13.0.9", + "ngx-color-picker": "^15.0.0", "ngx-extended-pdf-viewer": "^16.2.16", "ngx-file-drop": "^16.0.0", "ngx-slider-v2": "^16.0.2", diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index fedad5ccd..43be00b2f 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -120,7 +120,6 @@ export class AccountService { const user = response; if (user) { this.setCurrentUser(user); - this.messageHub.createHubConnection(user, this.hasAdminRole(user)); } }), takeUntilDestroyed(this.destroyRef) @@ -150,7 +149,8 @@ export class AccountService { this.stopRefreshTokenTimer(); if (this.currentUser) { - this.messageHub.createHubConnection(this.currentUser, this.hasAdminRole(this.currentUser)); + this.messageHub.stopHubConnection(); + this.messageHub.createHubConnection(this.currentUser); this.hasValidLicense().subscribe(); this.startRefreshTokenTimer(); } diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index 57279de18..a2a0e87ba 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -114,8 +114,6 @@ export class MessageHubService { */ public onlineUsers$ = this.onlineUsersSource.asObservable(); - isAdmin: boolean = false; - constructor() {} /** @@ -132,9 +130,7 @@ export class MessageHubService { return event.event === eventType; } - createHubConnection(user: User, isAdmin: boolean) { - this.isAdmin = isAdmin; - + createHubConnection(user: User) { this.hubConnection = new HubConnectionBuilder() .withUrl(this.hubUrl + 'messages', { accessTokenFactory: () => user.token @@ -186,7 +182,6 @@ export class MessageHubService { }); this.hubConnection.on(EVENTS.DashboardUpdate, resp => { - console.log('dashboard update event came in') this.messagesSource.next({ event: EVENTS.DashboardUpdate, payload: resp.body as DashboardUpdateEvent diff --git a/UI/Web/src/app/admin/license/license.component.ts b/UI/Web/src/app/admin/license/license.component.ts index 6c560de6e..b73d51beb 100644 --- a/UI/Web/src/app/admin/license/license.component.ts +++ b/UI/Web/src/app/admin/license/license.component.ts @@ -92,7 +92,7 @@ export class LicenseComponent implements OnInit { } async deleteLicense() { - if (!await this.confirmService.confirm(translate('k+-delete-key'))) { + if (!await this.confirmService.confirm(translate('toasts.k+-delete-key'))) { return; } diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.ts b/UI/Web/src/app/dashboard/_components/dashboard.component.ts index ece94bee5..89c04ec6e 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.ts @@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, injec import {Title} from '@angular/platform-browser'; import {Router, RouterLink} from '@angular/router'; import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs'; -import {map, shareReplay, take, tap, throttleTime} from 'rxjs/operators'; +import {debounceTime, map, shareReplay, take, tap, throttleTime} from 'rxjs/operators'; import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service'; import {Library} from 'src/app/_models/library'; import {RecentlyAddedItem} from 'src/app/_models/recently-added-item'; @@ -57,6 +57,7 @@ export class DashboardComponent implements OnInit { streams: Array = []; genre: Genre | undefined; refreshStreams$ = new Subject(); + refreshStreamsFromDashboardUpdate$ = new Subject(); /** @@ -80,6 +81,13 @@ export class DashboardComponent implements OnInit { this.loadDashboard(); + this.refreshStreamsFromDashboardUpdate$.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(1000), + tap(() => { + console.log('Loading Dashboard') + this.loadDashboard() + })) + .subscribe(); + this.refreshStreams$.pipe(takeUntilDestroyed(this.destroyRef), throttleTime(10_000), tap(() => { this.loadDashboard() @@ -87,29 +95,12 @@ export class DashboardComponent implements OnInit { .subscribe(); - // TODO: Solve how Websockets will work with these dyanamic streams this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => { - if (res.event === EVENTS.DashboardUpdate) { - console.log('dashboard update triggered') - this.refreshStreams$.next(); + this.refreshStreamsFromDashboardUpdate$.next(); } else if (res.event === EVENTS.SeriesAdded) { - // const seriesAddedEvent = res.payload as SeriesAddedEvent; - - // this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => { - // if (this.recentlyAddedSeries.filter(s => s.id === series.id).length > 0) return; - // this.recentlyAddedSeries = [series, ...this.recentlyAddedSeries]; - // this.cdRef.markForCheck(); - // }); this.refreshStreams$.next(); } else if (res.event === EVENTS.SeriesRemoved) { - //const seriesRemovedEvent = res.payload as SeriesRemovedEvent; - - // - // this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId); - // this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId); - // this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId); - // this.cdRef.markForCheck(); this.refreshStreams$.next(); } else if (res.event === EVENTS.ScanSeries) { // We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time. diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html index 6418b1e45..bd84c1b46 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html @@ -20,9 +20,9 @@ {{t('shortcuts-menu-alt')}} diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index aae5de538..3a9d98ae6 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -737,7 +737,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) { let img; - if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum); + if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum); else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum && (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1) ); @@ -1208,9 +1208,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { setCanvasImage() { if (this.cachedImages === undefined) return; this.canvasImage = this.getPage(this.pageNum, this.chapterId, this.layoutMode !== LayoutMode.Single); - this.canvasImage.addEventListener('load', () => { + if (!this.canvasImage.complete) { + this.canvasImage.addEventListener('load', () => { + this.currentImage.next(this.canvasImage); + }, false); + } else { this.currentImage.next(this.canvasImage); - }, false); + } + this.cdRef.markForCheck(); } @@ -1329,7 +1334,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - // const pages = this.cachedImages.map(img => [this.readerService.imageUrlToChapterId(img.src), this.readerService.imageUrlToPageNum(img.src)]); + //const pages = this.cachedImages.map(img => [this.readerService.imageUrlToChapterId(img.src), this.readerService.imageUrlToPageNum(img.src)]); // console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => { // if (this.pageNum === p[1]) return '[' + p + ']'; // return '' + p diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html index b783ba1a1..aa19d4c6d 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html +++ b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html @@ -36,7 +36,7 @@
-
+
diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss index 4abcdcdda..f4bb793e7 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss @@ -3,7 +3,7 @@ } -::ng-deep .ngb-dp-content, ::ng-deep .ngb-dp-header, ::ng-deep .dropdown-menu{ +::ng-deep .ngb-dp-content, ::ng-deep .ngb-dp-header{ background: var(--bs-body-bg); color: var(--body-text-color); } diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index 99210e5a1..fc314f8db 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -1,22 +1,26 @@ -
-
- -
-
- -
- -
- {{t('filter-title')}} -
-
- + +
+
- -
+
+ + +
+ +
+ {{t('filter-title')}} +
+
+ + +
+
+
+
+ @@ -30,13 +34,13 @@
-
+
-
+
-
+
- + + - - - +
@@ -73,11 +76,11 @@ -
+
-
+
  • - All Smart filters added to Dashboard or none created yet. + {{t('no-data')}}
  • diff --git a/UI/Web/src/app/sidenav/_components/customize-dashboard-modal/customize-dashboard-modal.component.ts b/UI/Web/src/app/sidenav/_components/customize-dashboard-modal/customize-dashboard-modal.component.ts index cd6850291..65190c874 100644 --- a/UI/Web/src/app/sidenav/_components/customize-dashboard-modal/customize-dashboard-modal.component.ts +++ b/UI/Web/src/app/sidenav/_components/customize-dashboard-modal/customize-dashboard-modal.component.ts @@ -36,7 +36,6 @@ export class CustomizeDashboardModalComponent { private readonly cdRef = inject(ChangeDetectorRef); constructor(public modal: NgbActiveModal) { - forkJoin([this.dashboardService.getDashboardStreams(false), this.filterService.getAllFilters()]).subscribe(results => { this.items = results[0]; const smartFilterStreams = new Set(results[0].filter(d => !d.isProvided).map(d => d.name)); diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 92589d2db..a551dee78 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -792,7 +792,7 @@ "allow-scrobbling-label": "Allow Scrobbling", "allow-scrobbling-tooltip": "Should Kavita scrobble reading events, want to read status, ratings, and reviews to configured providers. This will only occur if the server has an active Kavita+ Subscription.", "folder-watching-label": "Folder Watching", - "folder-watching-tooltip": "Override Server folder watching for this library. If off, folder watching won't run on the folders this library contains. If libraries share folders, then folders may still be ran against.", + "folder-watching-tooltip": "Override Server folder watching for this library. If off, folder watching won't run on the folders this library contains. If libraries share folders, then folders may still be ran against. Will always wait 10 minutes before triggering scan.", "include-in-dashboard-label": "Include in Dashboard", "include-in-dashboard-tooltip": "Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.", "include-in-recommendation-label": "Include in Recommended", @@ -1493,6 +1493,8 @@ "swipe-enabled-label": "Swipe Enabled", "enable-comic-book-label": "Emulate comic book", "brightness-label": "Brightness", + "bookmark-page-tooltip": "Bookmark Page", + "unbookmark-page-tooltip": "Unbookmark Page", "first-time-reading-manga": "Tap the image at any time to open the menu. You can configure different settings or go to page by clicking progress bar. Tap sides of image move to next/prev page.", "layout-mode-switched": "Layout mode switched to Single due to insufficient space to render double layout", @@ -1555,7 +1557,7 @@ "last-chapter-added": "Item Added", "time-to-read": "Time to Read", "release-year": "Release Year", - "read-progress": "Read Progress" + "read-progress": "Last Read" }, "edit-series-modal": { @@ -1728,8 +1730,10 @@ "customize-dashboard-modal": { "title": "Customize Dashboard", + "no-data": "All Smart filters added to Dashboard or none created yet.", "close": "{{common.close}}", - "save": "{{common.save}}" + "save": "{{common.save}}", + "add": "{{common.add}}" }, "filter-field-pipe": { diff --git a/UI/Web/src/environments/environment.prod.ts b/UI/Web/src/environments/environment.prod.ts index 54642eacc..37434aa27 100644 --- a/UI/Web/src/environments/environment.prod.ts +++ b/UI/Web/src/environments/environment.prod.ts @@ -5,6 +5,6 @@ export const environment = { production: true, apiUrl: `${BASE_URL}api/`, hubUrl:`${BASE_URL}hubs/`, - buyLink: 'https://buy.stripe.com/3cs7uw67p2Re7JK4gj?prefilled_promo_code=FREETRIAL', + buyLink: 'https://buy.stripe.com/00gcOQanFajG0hi5ko?prefilled_promo_code=FREETRIAL', manageLink: 'https://billing.stripe.com/p/login/28oaFRa3HdHWb5ecMM' }; diff --git a/openapi.json b/openapi.json index add343655..34dab8e1c 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.8.4" + "version": "0.7.8.5" }, "servers": [ {