diff --git a/API.Tests/Parser/BookParserTests.cs b/API.Tests/Parser/BookParserTests.cs index 7f6975fe5..cb91fc947 100644 --- a/API.Tests/Parser/BookParserTests.cs +++ b/API.Tests/Parser/BookParserTests.cs @@ -6,6 +6,7 @@ namespace API.Tests.Parser { [Theory] [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", "Gifting The Wonderful World With Blessings!")] + [InlineData("BBC Focus 00 The Science of Happiness 2nd Edition (2018)", "BBC Focus 00 The Science of Happiness 2nd Edition")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index 171e582cb..899cb8317 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -159,7 +159,6 @@ namespace API.Tests.Parser [InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "The 100 Girlfriends Who Really, Really, Really, Really, Really Love You")] [InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo")] [InlineData("The Duke of Death and His Black Maid - Ch. 177 - The Ball (3).cbz", "The Duke of Death and His Black Maid")] - [InlineData("A Compendium of Ghosts - 031 - The Third Story_ Part 12 (Digital) (Cobalt001)", "A Compendium of Ghosts")] [InlineData("The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake", "The Duke of Death and His Black Maid")] [InlineData("Vol. 04 Ch. 054.5", "")] [InlineData("Great_Teacher_Onizuka_v16[TheSpectrum]", "Great Teacher Onizuka")] @@ -168,6 +167,7 @@ namespace API.Tests.Parser [InlineData("Kaiju No. 8 036 (2021) (Digital)", "Kaiju No. 8")] [InlineData("Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz", "Seraph of the End - Vampire Reign")] [InlineData("Love Hina - Volume 01 [Scans].pdf", "Love Hina")] + [InlineData("It's Witching Time! 001 (Digital) (Anonymous1234)", "It's Witching Time!")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 5f862d35f..0026ea678 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -11,6 +11,7 @@ using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; using API.Services; +using API.SignalR; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -334,4 +335,65 @@ public class BookmarkServiceTests Assert.False(ds.FileSystem.FileInfo.FromFileName(Path.Join(BookmarkDirectory, "1/1/1/0001.jpg")).Exists); } #endregion + + #region GetBookmarkFilesById + + [Fact] + public async Task GetBookmarkFilesById_ShouldMatchActualFiles() + { + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123")); + + // Delete all Series to reset state + await ResetDB(); + + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + new Volume() + { + Chapters = new List() + { + new Chapter() + { + + } + } + } + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "Joe" + }); + + await _context.SaveChangesAsync(); + + + var ds = new DirectoryService(Substitute.For>(), filesystem); + var bookmarkService = new BookmarkService(Substitute.For>(), _unitOfWork, ds); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks); + + await bookmarkService.BookmarkPage(user, new BookmarkDto() + { + ChapterId = 1, + Page = 1, + SeriesId = 1, + VolumeId = 1 + }, $"{CacheDirectory}1/0001.jpg"); + + var files = await bookmarkService.GetBookmarkFilesById(1, new[] {1}); + var actualFiles = ds.GetFiles(BookmarkDirectory, searchOption: SearchOption.AllDirectories); + Assert.Equal(files.Select(API.Parser.Parser.NormalizePath).ToList(), actualFiles.Select(API.Parser.Parser.NormalizePath).ToList()); + } + + + #endregion } diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 7f1ead893..016b31f0f 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1568,7 +1568,7 @@ public class ReaderServiceTests } [Fact] - public async Task MarkChaptersUntilAsRead_ShouldNotReadOnlyVolumesWithChapter0() + public async Task MarkChaptersUntilAsRead_ShouldMarkAsRead_OnlyVolumesWithChapter0() { _context.Series.Add(new Series() { @@ -1604,7 +1604,7 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.False(await _unitOfWork.AppUserProgressRepository.UserHasProgress(LibraryType.Manga, 1)); + Assert.True(await _unitOfWork.AppUserProgressRepository.UserHasProgress(LibraryType.Manga, 1)); } #endregion diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index 30d0abd0f..5ae577f5e 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -7,7 +7,6 @@ using API.Constants; using API.Data; using API.DTOs.Downloads; using API.Entities; -using API.Entities.Enums; using API.Extensions; using API.Services; using API.SignalR; @@ -15,7 +14,6 @@ using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace API.Controllers @@ -30,10 +28,11 @@ namespace API.Controllers private readonly IEventHub _eventHub; private readonly UserManager _userManager; private readonly ILogger _logger; + private readonly IBookmarkService _bookmarkService; private const string DefaultContentType = "application/octet-stream"; public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, - IDownloadService downloadService, IEventHub eventHub, UserManager userManager, ILogger logger) + IDownloadService downloadService, IEventHub eventHub, UserManager userManager, ILogger logger, IBookmarkService bookmarkService) { _unitOfWork = unitOfWork; _archiveService = archiveService; @@ -42,6 +41,7 @@ namespace API.Controllers _eventHub = eventHub; _userManager = userManager; _logger = logger; + _bookmarkService = bookmarkService; } [HttpGet("volume-size")] @@ -172,13 +172,7 @@ namespace API.Controllers var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId); - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - - var files = (await _unitOfWork.UserRepository.GetAllBookmarksByIds(downloadBookmarkDto.Bookmarks - .Select(b => b.Id) - .ToList())) - .Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, $"{b.ChapterId}_{b.FileName}"))); + var files = await _bookmarkService.GetBookmarkFilesById(user.Id, downloadBookmarkDto.Bookmarks.Select(b => b.Id)); var filename = $"{series.Name} - Bookmarks.zip"; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, @@ -187,6 +181,8 @@ namespace API.Controllers $"download_{user.Id}_{series.Id}_bookmarks"); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 1F)); + + return File(fileBytes, DefaultContentType, filename); } diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index ac55e22fd..5cbb816e1 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -120,7 +120,7 @@ namespace API.Parser RegexTimeout), // Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto] new Regex( - @"(?.*)( - )(?:v|vo|c)\d", + @"(?.*)( - )(?:v|vo|c|chapters)\d", MatchOptions, RegexTimeout), // Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip new Regex( @@ -153,16 +153,7 @@ namespace API.Parser MatchOptions, RegexTimeout), // Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) new Regex( - @"(?.*) (\b|_|-)(v|ch\.?|c)\d+", - MatchOptions, RegexTimeout), - //Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip must be before [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip - // due to duplicate version identifiers in file. - new Regex( - @"(?.*)(v|s)\d+(-\d+)?(_|\s)", - MatchOptions, RegexTimeout), - //[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip - new Regex( - @"(?.*)(v|s)\d+(-\d+)?", + @"(?.*) (\b|_|-)(v|ch\.?|c|s)\d+", MatchOptions, RegexTimeout), // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz new Regex( @@ -170,7 +161,7 @@ namespace API.Parser MatchOptions, RegexTimeout), // Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire) new Regex( - @"(?.*) (?\d+(?:.\d+|-\d+)?) \(\d{4}\)", + @"(?.*) (-)?(?\d+(?:.\d+|-\d+)?) \(\d{4}\)", MatchOptions, RegexTimeout), // Noblesse - Episode 429 (74 Pages).7z new Regex( @@ -184,6 +175,23 @@ namespace API.Parser new Regex( @"(?.*)(\s|_)\((c\s|ch\s|chapter\s)", MatchOptions, RegexTimeout), + // Fullmetal Alchemist chapters 101-108 + new Regex( + @"(?.+?)(\s|_|\-)+?chapters(\s|_|\-)+?\d+(\s|_|\-)+?", + MatchOptions, RegexTimeout), + // It's Witching Time! 001 (Digital) (Anonymous1234) + new Regex( + @"(?.+?)(\s|_|\-)+?\d+(\s|_|\-)\(", + MatchOptions, RegexTimeout), + //Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip must be before [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip + // due to duplicate version identifiers in file. + new Regex( + @"(?.*)(v|s)\d+(-\d+)?(_|\s)", + MatchOptions, RegexTimeout), + //[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip + new Regex( + @"(?.*)(v|s)\d+(-\d+)?", + MatchOptions, RegexTimeout), // Black Bullet (This is very loose, keep towards bottom) new Regex( @"(?.*)(_)(v|vo|c|volume)( |_)\d+", diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs index 7acea6ad8..2f4cd8cdc 100644 --- a/API/Services/BookmarkService.cs +++ b/API/Services/BookmarkService.cs @@ -7,6 +7,7 @@ using API.Data; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; +using API.SignalR; using Microsoft.Extensions.Logging; namespace API.Services; @@ -16,6 +17,7 @@ public interface IBookmarkService Task DeleteBookmarkFiles(IEnumerable bookmarks); Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark); Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto); + Task> GetBookmarkFilesById(int userId, IEnumerable bookmarkIds); } public class BookmarkService : IBookmarkService @@ -139,6 +141,17 @@ public class BookmarkService : IBookmarkService return true; } + public async Task> GetBookmarkFilesById(int userId, IEnumerable bookmarkIds) + { + var bookmarkDirectory = + (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + + var bookmarks = await _unitOfWork.UserRepository.GetAllBookmarksByIds(bookmarkIds.ToList()); + return bookmarks + .Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, + b.FileName))); + } + private static string BookmarkStem(int userId, int seriesId, int chapterId) { return Path.Join($"{userId}", $"{seriesId}", $"{chapterId}"); diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 4568a2d84..a4e196ae9 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -394,7 +394,7 @@ public class ReaderService : IReaderService { var chapters = volume.Chapters .OrderBy(c => float.Parse(c.Number)) - .Where(c => !c.IsSpecial && Parser.Parser.MaximumNumberFromRange(c.Range) <= chapterNumber && Parser.Parser.MaximumNumberFromRange(c.Range) > 0.0); + .Where(c => !c.IsSpecial && Parser.Parser.MaximumNumberFromRange(c.Range) <= chapterNumber); MarkChaptersAsRead(user, volume.SeriesId, chapters); } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 17e3b0698..422d98e66 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -105,6 +105,8 @@ public class SeriesService : ISeriesService updateSeriesMetadataDto.SeriesMetadata.SeriesId), false); } + await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); + return true; } } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index bb98f2495..769dda73f 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -439,6 +439,11 @@ public class ScannerService : IScannerService try { await _unitOfWork.CommitAsync(); + + // Update the people, genres, and tags after committing as we might have inserted new ones. + allPeople = await _unitOfWork.PersonRepository.GetAllPeople(); + allGenres = await _unitOfWork.GenreRepository.GetAllGenresAsync(); + allTags = await _unitOfWork.TagRepository.GetAllTagsAsync(); } catch (Exception ex) { @@ -463,7 +468,6 @@ public class ScannerService : IScannerService foreach (var series in librarySeries) { - // TODO: Do I need this? Shouldn't this be NotificationProgress // This is something more like, the series has finished updating in the backend. It may or may not have been modified. await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(series.Id, series.Name)); } @@ -882,9 +886,9 @@ public class ScannerService : IScannerService chapter.TotalCount = comicInfo.Count; } - if (!string.IsNullOrEmpty(comicInfo.Number) && int.Parse(comicInfo.Number) > 0) + if (!string.IsNullOrEmpty(comicInfo.Number) && float.Parse(comicInfo.Number) > 0) { - chapter.Count = int.Parse(comicInfo.Number); + chapter.Count = (int) Math.Floor(float.Parse(comicInfo.Number)); } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index a84d1ba2e..e6a43a1be 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -156,7 +156,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.pagination?.currentPage}_${this.updateApplied}`; + this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.pagination?.currentPage}_${this.updateApplied}_${item?.libraryId}`; + if (this.filterSettings === undefined) { this.filterSettings = new FilterSettings(); diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html index 9edfe9c44..ca87d201d 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html @@ -6,15 +6,15 @@
- +
-
- Enter a Url - +
+ Enter a Url + Drag and drop - + Upload an image
@@ -24,17 +24,14 @@
-
- -
- -
- +
+ +
- diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts index 0c0830b16..c6e3d895c 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts @@ -103,6 +103,14 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy { this.form.get('coverImageUrl')?.setValue(''); } } + + changeMode(mode: 'url') { + this.mode = mode; + this.setupEnterHandler(); + setTimeout(() => { + + }) + } diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index 7be1da17b..88374a312 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -892,6 +892,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (!this.shouldRenderAsFitSplit()) { this.setCanvasSize(); this.ctx.drawImage(this.canvasImage, 0, 0); + + // Reset scroll on non HEIGHT Fits + if (this.getFit() !== FITTING_OPTION.HEIGHT) { + window.scrollTo(0, 0); + } + this.isLoading = false; return; }