From 567d475e46509c40e2cbf3f0abf42d0cc3ca4bbb Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Fri, 28 Jan 2022 06:29:49 -0800 Subject: [PATCH] Bugfix/tachiyomi sync issue (#1002) * Added extra unit cases for GetContinuePoint. Fixed a bug where if the series was just read chapters, the first chapter wouldn't be returned and would throw an error. * Wrote unit tests for MarkChaptersUntilAsRead for Tachiyomi and fixed a few cases where due to tracking on Tachiyomi, Volumes with a single 0 chapter would get marked as read. --- API.Tests/Services/ReaderServiceTests.cs | 276 +++++++++++++++++++++++ API/Controllers/ReaderController.cs | 7 +- API/Services/ReaderService.cs | 44 ++-- 3 files changed, 306 insertions(+), 21 deletions(-) diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index d47c891db..232a93c4e 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1007,6 +1007,282 @@ public class ReaderServiceTests Assert.Equal("1", nextChapter.Range); } + [Fact] + public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllReadAndAllChapters() + { + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("0", new List() + { + EntityFactory.CreateChapter("1", false, new List(), 1), + EntityFactory.CreateChapter("2", false, new List(), 1), + EntityFactory.CreateChapter("3", false, new List(), 1), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var fileSystem = new MockFileSystem(); + var ds = new DirectoryService(Substitute.For>(), fileSystem); + var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); + + // Save progress on first volume chapters and 1st of second volume + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 1, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 2, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 3, + SeriesId = 1, + VolumeId = 1 + }, 1); + + await _context.SaveChangesAsync(); + + var nextChapter = await readerService.GetContinuePoint(1, 1); + + Assert.Equal("1", nextChapter.Range); + } + + [Fact] + public async Task GetContinuePoint_ShouldReturnFirstSpecial_WhenAllReadAndAllChapters() + { + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("0", new List() + { + EntityFactory.CreateChapter("1", false, new List(), 1), + EntityFactory.CreateChapter("2", false, new List(), 1), + EntityFactory.CreateChapter("3", false, new List(), 1), + EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var fileSystem = new MockFileSystem(); + var ds = new DirectoryService(Substitute.For>(), fileSystem); + var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); + + // Save progress on first volume chapters and 1st of second volume + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 1, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 2, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 3, + SeriesId = 1, + VolumeId = 1 + }, 1); + + await _context.SaveChangesAsync(); + + var nextChapter = await readerService.GetContinuePoint(1, 1); + + Assert.Equal("Some Special Title", nextChapter.Range); + } + + #endregion + + #region MarkChaptersUntilAsRead + + [Fact] + public async Task MarkChaptersUntilAsRead_ShouldMarkAllChaptersAsRead() + { + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("0", new List() + { + EntityFactory.CreateChapter("1", false, new List(), 1), + EntityFactory.CreateChapter("2", false, new List(), 1), + EntityFactory.CreateChapter("3", false, new List(), 1), + EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var fileSystem = new MockFileSystem(); + var ds = new DirectoryService(Substitute.For>(), fileSystem); + var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); + + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + await readerService.MarkChaptersUntilAsRead(user, 1, 5); + await _context.SaveChangesAsync(); + + // Validate correct chapters have read status + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); + } + + [Fact] + public async Task MarkChaptersUntilAsRead_ShouldMarkUptTillChapterNumberAsRead() + { + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("0", new List() + { + EntityFactory.CreateChapter("1", false, new List(), 1), + EntityFactory.CreateChapter("2", false, new List(), 1), + EntityFactory.CreateChapter("2.5", false, new List(), 1), + EntityFactory.CreateChapter("3", false, new List(), 1), + EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var fileSystem = new MockFileSystem(); + var ds = new DirectoryService(Substitute.For>(), fileSystem); + var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); + + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + await readerService.MarkChaptersUntilAsRead(user, 1, 2.5f); + await _context.SaveChangesAsync(); + + // Validate correct chapters have read status + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); + Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); + Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1))); + } + + [Fact] + public async Task MarkChaptersUntilAsRead_ShouldNotReadOnlyVolumesWithChapter0() + { + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("1", new List() + { + EntityFactory.CreateChapter("0", false, new List(), 1), + }), + EntityFactory.CreateVolume("2", new List() + { + EntityFactory.CreateChapter("0", false, new List(), 1), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var fileSystem = new MockFileSystem(); + var ds = new DirectoryService(Substitute.For>(), fileSystem); + var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); + + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + await readerService.MarkChaptersUntilAsRead(user, 1, 2); + await _context.SaveChangesAsync(); + + // Validate correct chapters have read status + Assert.False(await _unitOfWork.AppUserProgressRepository.UserHasProgress(LibraryType.Manga, 1)); + } + #endregion diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 4e0b8291e..4c8465b21 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -392,12 +392,7 @@ namespace API.Controllers var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); user.Progresses ??= new List(); - var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List() { seriesId }, true); - foreach (var volume in volumes.OrderBy(v => v.Number)) - { - var chapters = volume.Chapters.OrderBy(c => float.Parse(c.Number)).Where(c => !c.IsSpecial && Parser.Parser.MaximumNumberFromRange(c.Range) <= chapterNumber); - _readerService.MarkChaptersAsRead(user, volume.SeriesId, chapters); - } + await _readerService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber); _unitOfWork.UserRepository.Update(user); diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index a929165ec..be43c78cf 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -23,6 +23,7 @@ public interface IReaderService Task GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); Task GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); Task GetContinuePoint(int seriesId, int userId); + Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber); } public class ReaderService : IReaderService @@ -317,27 +318,22 @@ public class ReaderService : IReaderService .OrderBy(c => float.Parse(c.Number)) .ToList(); - - var currentlyReadingChapter = nonSpecialChapters.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages); - // Check if there are any specials - if (currentlyReadingChapter == null) - { - var volume = volumes.SingleOrDefault(v => v.Number == 0); - if (volume == null) return nonSpecialChapters.First(); + if (currentlyReadingChapter != null) return currentlyReadingChapter; - foreach (var chapter in volume.Chapters.OrderBy(c => float.Parse(c.Number))) - { - if (chapter.PagesRead < chapter.Pages) - { - return chapter; - } - } + // Check if there are any specials + var volume = volumes.SingleOrDefault(v => v.Number == 0); + if (volume == null) return nonSpecialChapters.First(); + + var chapters = volume.Chapters.OrderBy(c => float.Parse(c.Number)).ToList(); + foreach (var chapter in chapters.Where(chapter => chapter.PagesRead < chapter.Pages)) + { + return chapter; } - return currentlyReadingChapter ?? nonSpecialChapters.First(); + return chapters.First(); } @@ -357,5 +353,23 @@ public class ReaderService : IReaderService return -1; } + /// + /// Marks every chapter that is sorted below the passed number as Read. This will not mark any specials as read or Volumes with a single 0 chapter. + /// + /// + /// + /// + public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber) + { + var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List() { seriesId }, true); + foreach (var volume in volumes.OrderBy(v => v.Number)) + { + 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); + MarkChaptersAsRead(user, volume.SeriesId, chapters); + } + } + }