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 @@