From 428516b2246e0b2333da5b1acd7228eb8c28b183 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 5 Apr 2022 09:39:03 -0500 Subject: [PATCH] Post Release Shakeout (Hotfix Testing) (#1200) * Fixed an issue where when falling back to folder parsing, sometimes the folder name wouldn't parse well, like "Foo 50" which parses as "Foo". Now the fallback will check if we have a solid series parsed from filename before we attempt to parse a folder. * Ensure SortName is set during a scan loop even if locked and it's empty string. * Added some null checks for metadata update * Fixed a bug where Updating a series name with a name of an existing series wouldn't properly check for existing series. * Tweaked the logic of OnDeck to consider LastChapterCreated from all chapters in a series, not just those with progress. * Fixed a bug where the hamburger menu was still visible on login/registration page despite not functioning * Tweaked the logic of OnDeck to consider LastChapterCreated from all chapters in a series, not just those with progress. * Removed 2 unused packages from ui * Fixed some bugs around determining what the current installed version is in Announcements * Use AnyAsync for a query to improve performance * Fixed up some fallback code * Tests are finally fixed --- API.Tests/Parser/DefaultParserTests.cs | 97 ++++++++++++++++++- API/Controllers/SeriesController.cs | 11 ++- API/Data/Repositories/SeriesRepository.cs | 21 ++-- API/Entities/Metadata/SeriesMetadata.cs | 2 +- API/Parser/DefaultParser.cs | 24 +++-- API/Services/SeriesService.cs | 11 +++ API/Services/Tasks/ScannerService.cs | 6 +- API/Services/Tasks/VersionUpdaterService.cs | 7 +- UI/Web/package-lock.json | 16 --- UI/Web/package.json | 2 - .../app/_interceptors/error.interceptor.ts | 5 + .../changelog/changelog.component.html | 11 ++- .../changelog/changelog.component.ts | 15 +-- .../app/nav-header/nav-header.component.html | 2 +- .../confirm-email/confirm-email.component.ts | 22 ++--- .../shared/read-more/read-more.component.html | 4 +- .../sidenav/side-nav/side-nav.component.ts | 3 +- .../app/user-login/user-login.component.ts | 5 +- 18 files changed, 179 insertions(+), 85 deletions(-) diff --git a/API.Tests/Parser/DefaultParserTests.cs b/API.Tests/Parser/DefaultParserTests.cs index a9c593de9..200a6b16a 100644 --- a/API.Tests/Parser/DefaultParserTests.cs +++ b/API.Tests/Parser/DefaultParserTests.cs @@ -44,7 +44,7 @@ public class DefaultParserTests [Theory] [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")] [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!~1~2")] - [InlineData("/manga/Monster #8/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster #8~0~1")] + [InlineData("/manga/Monster/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster~0~1")] public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string expectedParseInfo) { const string rootDirectory = "/manga/"; @@ -56,6 +56,27 @@ public class DefaultParserTests Assert.Equal(tokens[2], actual.Chapters); } + [Theory] + [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!")] + [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!")] + [InlineData("/manga/Monster #8 (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")] + [InlineData("/manga/Monster (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")] + [InlineData("/manga/Foo 50/Specials/Foo 50 SP01.cbz", "Foo 50")] + [InlineData("/manga/Foo 50 (kiraa)/Specials/Foo 50 SP01.cbz", "Foo 50")] + [InlineData("/manga/Btooom!/Specials/Just a special SP01.cbz", "Btooom!")] + public void ParseFromFallbackFolders_ShouldUseExistingSeriesName(string inputFile, string expectedParseInfo) + { + const string rootDirectory = "/manga/"; + var fs = new MockFileSystem(); + fs.AddDirectory(rootDirectory); + fs.AddFile(inputFile, new MockFileData("")); + var ds = new DirectoryService(Substitute.For>(), fs); + var parser = new DefaultParser(ds); + var actual = parser.Parse(inputFile, rootDirectory); + _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); + Assert.Equal(expectedParseInfo, actual.Series); + } + #endregion @@ -243,6 +264,80 @@ public class DefaultParserTests } } + [Fact] + public void Parse_ParseInfo_Manga_WithSpecialsFolder() + { + const string rootPath = @"E:/Manga/"; + var filesystem = new MockFileSystem(); + filesystem.AddDirectory("E:/Manga"); + filesystem.AddDirectory("E:/Foo 50"); + filesystem.AddDirectory("E:/Foo 50/Specials"); + filesystem.AddFile(@"E:/Manga/Foo 50/Foo 50 v1.cbz", new MockFileData("")); + filesystem.AddFile(@"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz", new MockFileData("")); + + var ds = new DirectoryService(Substitute.For>(), filesystem); + var parser = new DefaultParser(ds); + + var filepath = @"E:/Manga/Foo 50/Foo 50 v1.cbz"; + // There is a bad parse for series like "Foo 50", so we have parsed chapter as 50 + var expected = new ParserInfo + { + Series = "Foo 50", Volumes = "1", + Chapters = "50", Filename = "Foo 50 v1.cbz", Format = MangaFormat.Archive, + FullFilePath = filepath + }; + + var actual = parser.Parse(filepath, rootPath); + + Assert.NotNull(actual); + _testOutputHelper.WriteLine($"Validating {filepath}"); + Assert.Equal(expected.Format, actual.Format); + _testOutputHelper.WriteLine("Format ✓"); + Assert.Equal(expected.Series, actual.Series); + _testOutputHelper.WriteLine("Series ✓"); + Assert.Equal(expected.Chapters, actual.Chapters); + _testOutputHelper.WriteLine("Chapters ✓"); + Assert.Equal(expected.Volumes, actual.Volumes); + _testOutputHelper.WriteLine("Volumes ✓"); + Assert.Equal(expected.Edition, actual.Edition); + _testOutputHelper.WriteLine("Edition ✓"); + Assert.Equal(expected.Filename, actual.Filename); + _testOutputHelper.WriteLine("Filename ✓"); + Assert.Equal(expected.FullFilePath, actual.FullFilePath); + _testOutputHelper.WriteLine("FullFilePath ✓"); + Assert.Equal(expected.IsSpecial, actual.IsSpecial); + _testOutputHelper.WriteLine("IsSpecial ✓"); + + filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz"; + expected = new ParserInfo + { + Series = "Foo 50", Volumes = "0", IsSpecial = true, + Chapters = "50", Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive, + FullFilePath = filepath + }; + + actual = parser.Parse(filepath, rootPath); + Assert.NotNull(actual); + _testOutputHelper.WriteLine($"Validating {filepath}"); + Assert.Equal(expected.Format, actual.Format); + _testOutputHelper.WriteLine("Format ✓"); + Assert.Equal(expected.Series, actual.Series); + _testOutputHelper.WriteLine("Series ✓"); + Assert.Equal(expected.Chapters, actual.Chapters); + _testOutputHelper.WriteLine("Chapters ✓"); + Assert.Equal(expected.Volumes, actual.Volumes); + _testOutputHelper.WriteLine("Volumes ✓"); + Assert.Equal(expected.Edition, actual.Edition); + _testOutputHelper.WriteLine("Edition ✓"); + Assert.Equal(expected.Filename, actual.Filename); + _testOutputHelper.WriteLine("Filename ✓"); + Assert.Equal(expected.FullFilePath, actual.FullFilePath); + _testOutputHelper.WriteLine("FullFilePath ✓"); + Assert.Equal(expected.IsSpecial, actual.IsSpecial); + _testOutputHelper.WriteLine("IsSpecial ✓"); + + } + [Fact] public void Parse_ParseInfo_Comic() { diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 28751dbc1..3aad34d99 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -145,13 +145,20 @@ namespace API.Controllers if (series == null) return BadRequest("Series does not exist"); - if (series.Name != updateSeries.Name && await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name, series.Format)) + var seriesExists = + await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name.Trim(), series.LibraryId, + series.Format); + if (series.Name != updateSeries.Name && seriesExists) { return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library."); } series.Name = updateSeries.Name.Trim(); - series.SortName = updateSeries.SortName.Trim(); + if (!string.IsNullOrEmpty(updateSeries.SortName.Trim())) + { + series.SortName = updateSeries.SortName.Trim(); + } + series.LocalizedName = updateSeries.LocalizedName.Trim(); series.NameLocked = updateSeries.NameLocked; diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index d9252f97b..bf634e5c7 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -47,7 +47,7 @@ public interface ISeriesRepository void Update(Series series); void Remove(Series series); void Remove(IEnumerable series); - Task DoesSeriesNameExistInLibrary(string name, MangaFormat format); + Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format); /// /// Adds user information like progress, ratings, etc /// @@ -135,17 +135,12 @@ public class SeriesRepository : ISeriesRepository /// Name of series /// Format of series /// - public async Task DoesSeriesNameExistInLibrary(string name, MangaFormat format) + public async Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format) { - var libraries = _context.Series - .AsNoTracking() - .Where(x => x.Name.Equals(name) && x.Format == format) - .Select(s => s.LibraryId); - return await _context.Series .AsNoTracking() - .Where(s => libraries.Contains(s.LibraryId) && s.Name.Equals(name) && s.Format == format) - .CountAsync() > 1; + .Where(s => s.LibraryId == libraryId && s.Name.Equals(name) && s.Format == format) + .AnyAsync(); } @@ -624,13 +619,13 @@ public class SeriesRepository : ISeriesRepository LastReadingProgress = _context.AppUserProgresses .Where(p => p.Id == progress.Id && p.AppUserId == userId) .Max(p => p.LastModified), - // BUG: This is only taking into account chapters that have progress on them, not all chapters in said series - LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created), - //LastChapterCreated = _context.Chapter.Where(c => allChapters.Contains(c.Id)).Max(c => c.Created) + // This is only taking into account chapters that have progress on them, not all chapters in said series + //LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created), + LastChapterCreated = s.Volumes.SelectMany(v => v.Chapters).Max(c => c.Created) }); if (cutoffOnDate) { - query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint); + query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint || d.LastChapterCreated >= cutoffProgressPoint); } // I think I need another Join statement. The problem is the chapters are still limited to progress diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index 0ec7038fa..522f15d06 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -11,7 +11,7 @@ namespace API.Entities.Metadata { public int Id { get; set; } - public string Summary { get; set; } + public string Summary { get; set; } = string.Empty; public ICollection CollectionTags { get; set; } diff --git a/API/Parser/DefaultParser.cs b/API/Parser/DefaultParser.cs index 23b5c1d58..d03bff199 100644 --- a/API/Parser/DefaultParser.cs +++ b/API/Parser/DefaultParser.cs @@ -142,18 +142,22 @@ public class DefaultParser } } - var series = Parser.ParseSeries(folder); - - if ((string.IsNullOrEmpty(series) && i == fallbackFolders.Count - 1)) + // Generally users group in series folders. Let's try to parse series from the top folder + if (!folder.Equals(ret.Series) && i == fallbackFolders.Count - 1) { - ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic); - break; - } + var series = Parser.ParseSeries(folder); - if (!string.IsNullOrEmpty(series)) - { - ret.Series = series; - break; + if (string.IsNullOrEmpty(series)) + { + ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic); + break; + } + + if (!string.IsNullOrEmpty(series) && (string.IsNullOrEmpty(ret.Series) || !folder.Contains(ret.Series))) + { + ret.Series = series; + break; + } } } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index c1c9386dc..25f736e99 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -67,6 +67,17 @@ public class SeriesService : ISeriesService series.Metadata.PublicationStatusLocked = true; } + // This shouldn't be needed post v0.5.3 release + if (string.IsNullOrEmpty(series.Metadata.Summary)) + { + series.Metadata.Summary = string.Empty; + } + + if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary)) + { + updateSeriesMetadataDto.SeriesMetadata.Summary = string.Empty; + } + if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim()) { series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim(); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 6fe4a57c8..85f01aa09 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -523,13 +523,17 @@ public class ScannerService : IScannerService series.Format = parsedInfos[0].Format; } series.OriginalName ??= parsedInfos[0].Series; + if (string.IsNullOrEmpty(series.SortName)) + { + series.SortName = series.Name; + } if (!series.SortNameLocked) { + series.SortName = series.Name; if (!string.IsNullOrEmpty(parsedInfos[0].SeriesSort)) { series.SortName = parsedInfos[0].SeriesSort; } - series.SortName = series.Name; } await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name)); diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 5641b0962..8d79b3a45 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -92,12 +92,7 @@ public class VersionUpdaterService : IVersionUpdaterService { if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null; var updateVersion = new Version(update.Tag_Name.Replace("v", string.Empty)); - var currentVersion = BuildInfo.Version.ToString(); - - if (updateVersion.Revision == -1) - { - currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".", StringComparison.Ordinal)); - } + var currentVersion = BuildInfo.Version.ToString(4); return new UpdateNotificationDto() { diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 279f64bf3..c0b0ce2cd 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -2418,22 +2418,6 @@ "integrity": "sha512-wooUZiV92QyoeFxkhqIwH/cfiAAAn+l8fEEuaaEIfJtpjpbShvvlboEVsqb28soeGiFJfLcmsZM3mUFgsG4QBQ==", "dev": true }, - "@ngx-lite/nav-drawer": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/@ngx-lite/nav-drawer/-/nav-drawer-0.4.7.tgz", - "integrity": "sha512-OqXJhzE88RR5Vtgr0tcuvkKVkzsKZjeXxhjpWOJ9UiC2iCQPDL2rtvag5K/vPKN362Jx0htvr3cmFDjHg/kjdA==", - "requires": { - "tslib": "^2.0.0" - } - }, - "@ngx-lite/util": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@ngx-lite/util/-/util-0.0.1.tgz", - "integrity": "sha512-j7pBcF+5OEHExEUBNdlQT5x4sVvHIPwZeMvhlO1TAcAAz9frDsvYgJ1c3eXJYJKJq57o1rH1RESKSJ9YRNpAiw==", - "requires": { - "tslib": "^2.1.0" - } - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index 2dbba8f3f..235cbd85f 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -29,8 +29,6 @@ "@fortawesome/fontawesome-free": "^6.0.0", "@microsoft/signalr": "^6.0.2", "@ng-bootstrap/ng-bootstrap": "^12.0.0", - "@ngx-lite/nav-drawer": "^0.4.7", - "@ngx-lite/util": "0.0.1", "@popperjs/core": "^2.11.2", "@types/file-saver": "^2.0.5", "bootstrap": "^5.1.2", diff --git a/UI/Web/src/app/_interceptors/error.interceptor.ts b/UI/Web/src/app/_interceptors/error.interceptor.ts index a0b792045..8c3cdca61 100644 --- a/UI/Web/src/app/_interceptors/error.interceptor.ts +++ b/UI/Web/src/app/_interceptors/error.interceptor.ts @@ -116,6 +116,11 @@ export class ErrorInterceptor implements HttpInterceptor { } private handleAuthError(error: any) { + + // Special hack for register url, to not care about auth + if (location.href.includes('/registration/confirm-email?token=')) { + return; + } // NOTE: Signin has error.error or error.statusText available. // if statement is due to http/2 spec issue: https://github.com/angular/angular/issues/23334 this.accountService.logout(); diff --git a/UI/Web/src/app/announcements/changelog/changelog.component.html b/UI/Web/src/app/announcements/changelog/changelog.component.html index caffe5d8c..0a8b8a1b2 100644 --- a/UI/Web/src/app/announcements/changelog/changelog.component.html +++ b/UI/Web/src/app/announcements/changelog/changelog.component.html @@ -1,19 +1,20 @@
+

If you do not see an Installed tag, you are on a nightly release. Only major versions will show as available.

{{update.updateTitle}}  - Installed - Available + Installed + Available

-
Published: {{update.publishDate | date: 'short'}}
+
Published: {{update.publishDate | date: 'short'}}
             
           
- Installed - Download + Installed + Download
diff --git a/UI/Web/src/app/announcements/changelog/changelog.component.ts b/UI/Web/src/app/announcements/changelog/changelog.component.ts index 86724e92e..1df65496c 100644 --- a/UI/Web/src/app/announcements/changelog/changelog.component.ts +++ b/UI/Web/src/app/announcements/changelog/changelog.component.ts @@ -11,23 +11,14 @@ export class ChangelogComponent implements OnInit { updates: Array = []; isLoading: boolean = true; - installedVersion: string = ''; constructor(private serverService: ServerService) { } ngOnInit(): void { - this.serverService.getServerInfo().subscribe(info => { - this.installedVersion = info.kavitaVersion; - this.serverService.getChangelog().subscribe(updates => { - this.updates = updates; - this.isLoading = false; - - if (this.updates.filter(u => u.updateVersion === this.installedVersion).length === 0) { - // User is on a nightly version. Tell them the last stable is installed - this.installedVersion = this.updates[0].updateVersion; - } - }); + this.serverService.getChangelog().subscribe(updates => { + this.updates = updates; + this.isLoading = false; }); diff --git a/UI/Web/src/app/nav-header/nav-header.component.html b/UI/Web/src/app/nav-header/nav-header.component.html index c4149cd25..d61e6bc60 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav-header/nav-header.component.html @@ -1,7 +1,7 @@