diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 3b4b8cb94..708e253c0 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -7,10 +7,10 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API.Tests/Entities/ComicInfoTests.cs b/API.Tests/Entities/ComicInfoTests.cs index 7b7106eb9..325299cf8 100644 --- a/API.Tests/Entities/ComicInfoTests.cs +++ b/API.Tests/Entities/ComicInfoTests.cs @@ -16,7 +16,7 @@ public class ComicInfoTests [InlineData("Early Childhood", AgeRating.EarlyChildhood)] [InlineData("Everyone 10+", AgeRating.Everyone10Plus)] [InlineData("M", AgeRating.Mature)] - [InlineData("MA 15+", AgeRating.Mature15Plus)] + [InlineData("MA15+", AgeRating.Mature15Plus)] [InlineData("Mature 17+", AgeRating.Mature17Plus)] [InlineData("Rating Pending", AgeRating.RatingPending)] [InlineData("X18+", AgeRating.X18Plus)] diff --git a/API/API.csproj b/API/API.csproj index 492430a44..f3389e2d7 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -39,40 +39,40 @@ - - - + + + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index 89b2d3de4..e1207f919 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -40,7 +40,7 @@ namespace API.Controllers if (dto.SeriesFormat == MangaFormat.Epub) { var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First(); - using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath); + using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions); bookTitle = book.Title; } @@ -63,7 +63,7 @@ namespace API.Controllers public async Task GetBookPageResources(int chapterId, [FromQuery] string file) { var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath); + using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions); var key = BookService.CleanContentKeys(file); if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book"); @@ -87,7 +87,7 @@ namespace API.Controllers public async Task>> GetBookChapters(int chapterId) { var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath); + using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions); var mappings = await _bookService.CreateKeyToPageMappingAsync(book); var navItems = await book.GetNavigationAsync(); @@ -212,7 +212,7 @@ namespace API.Controllers var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter); - using var book = await EpubReader.OpenBookAsync(path); + using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions); var mappings = await _bookService.CreateKeyToPageMappingAsync(book); var counter = 0; diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs index 246caa04a..0c236fd58 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/API/Data/Metadata/ComicInfo.cs @@ -14,6 +14,10 @@ namespace API.Data.Metadata public string Summary { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string Series { get; set; } = string.Empty; + /// + /// Localized Series name. Not standard. + /// + public string LocalizedSeries { get; set; } = string.Empty; public string SeriesSort { get; set; } = string.Empty; public string Number { get; set; } = string.Empty; /// @@ -94,6 +98,10 @@ namespace API.Data.Metadata { if (info == null) return; + info.Series = info.Series.Trim(); + info.SeriesSort = info.SeriesSort.Trim(); + info.LocalizedSeries = info.LocalizedSeries.Trim(); + info.Writer = Parser.Parser.CleanAuthor(info.Writer); info.Colorist = Parser.Parser.CleanAuthor(info.Colorist); info.Editor = Parser.Parser.CleanAuthor(info.Editor); diff --git a/API/Entities/Enums/AgeRating.cs b/API/Entities/Enums/AgeRating.cs index ed9deac25..82dbef7ae 100644 --- a/API/Entities/Enums/AgeRating.cs +++ b/API/Entities/Enums/AgeRating.cs @@ -27,7 +27,7 @@ public enum AgeRating KidsToAdults = 7, [Description("Teen")] Teen = 8, - [Description("MA 15+")] + [Description("MA15+")] Mature15Plus = 9, [Description("Mature 17+")] Mature17Plus = 10, diff --git a/API/Parser/ParserInfo.cs b/API/Parser/ParserInfo.cs index 07679ea25..caae49f84 100644 --- a/API/Parser/ParserInfo.cs +++ b/API/Parser/ParserInfo.cs @@ -22,6 +22,10 @@ namespace API.Parser /// public string SeriesSort { get; set; } = string.Empty; /// + /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the LocalizedName field on + /// + public string LocalizedSeries { get; set; } = string.Empty; + /// /// Represents the parsed volumes from a file. By default, will be 0 which means that nothing could be parsed. /// If Volumes is 0 and Chapters is 0, the file is a special. If Chapters is non-zero, then no volume could be parsed. /// Beastars Vol 3-4 will map to "3-4" diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 9e9aa0ac2..a8627a200 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -20,6 +20,7 @@ using Microsoft.IO; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using VersOne.Epub; +using VersOne.Epub.Options; using Image = SixLabors.ImageSharp.Image; namespace API.Services @@ -59,6 +60,13 @@ namespace API.Services private readonly StylesheetParser _cssParser = new (); private static readonly RecyclableMemoryStreamManager StreamManager = new (); private const string CssScopeClass = ".book-content"; + public static readonly EpubReaderOptions BookReaderOptions = new() + { + PackageReaderOptions = new PackageReaderOptions() + { + IgnoreMissingToc = true + } + }; public BookService(ILogger logger, IDirectoryService directoryService, IImageService imageService) { @@ -383,10 +391,14 @@ namespace API.Services try { - using var epubBook = EpubReader.OpenBook(filePath); + using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); var publicationDate = epubBook.Schema.Package.Metadata.Dates.FirstOrDefault(date => date.Event == "publication")?.Date; + if (string.IsNullOrEmpty(publicationDate)) + { + publicationDate = epubBook.Schema.Package.Metadata.Dates.FirstOrDefault()?.Date; + } var info = new ComicInfo() { Summary = epubBook.Schema.Package.Metadata.Description, @@ -450,7 +462,7 @@ namespace API.Services return docReader.GetPageCount(); } - using var epubBook = EpubReader.OpenBook(filePath); + using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); return epubBook.Content.Html.Count; } catch (Exception ex) @@ -504,7 +516,7 @@ namespace API.Services try { - using var epubBook = EpubReader.OpenBook(filePath); + using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); // // @@ -669,8 +681,7 @@ namespace API.Services return GetPdfCoverImage(fileFilePath, fileName, outputDirectory); } - using var epubBook = EpubReader.OpenBook(fileFilePath); - + using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions); try { diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 4580c8d95..2df8d0554 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -254,7 +254,6 @@ public class SeriesService : ISeriesService // At this point, all tags that aren't in dto have been removed. foreach (var tagTitle in tags.Select(t => t.Title)) { - // This should be normalized name var normalizedTitle = Parser.Parser.Normalize(tagTitle); var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle == normalizedTitle); if (existingTag != null) @@ -299,10 +298,11 @@ public class SeriesService : ISeriesService // At this point, all tags that aren't in dto have been removed. foreach (var tagTitle in tags.Select(t => t.Title)) { - var existingTag = allTags.SingleOrDefault(t => t.Title == tagTitle); + var normalizedTitle = Parser.Parser.Normalize(tagTitle); + var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle)); if (existingTag != null) { - if (series.Metadata.Tags.All(t => t.Title != tagTitle)) + if (series.Metadata.Tags.All(t => t.NormalizedTitle != normalizedTitle)) { handleAdd(existingTag); diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 2ed3a65d4..e2106049f 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -126,6 +126,11 @@ namespace API.Services.Tasks.Scanner { info.SeriesSort = info.ComicInfo.SeriesSort.Trim(); } + + if (!string.IsNullOrEmpty(info.ComicInfo.LocalizedSeries)) + { + info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim(); + } } TrackSeries(info); @@ -144,13 +149,16 @@ namespace API.Services.Tasks.Scanner // Check if normalized info.Series already exists and if so, update info to use that name instead info.Series = MergeName(info); + var normalizedSeries = Parser.Parser.Normalize(info.Series); + var normalizedLocalizedSeries = Parser.Parser.Normalize(info.LocalizedSeries); var existingKey = _scannedSeries.Keys.FirstOrDefault(ps => - ps.Format == info.Format && ps.NormalizedName == Parser.Parser.Normalize(info.Series)); + ps.Format == info.Format && (ps.NormalizedName == normalizedSeries + || ps.NormalizedName == normalizedLocalizedSeries)); existingKey ??= new ParsedSeries() { Format = info.Format, Name = info.Series, - NormalizedName = Parser.Parser.Normalize(info.Series) + NormalizedName = normalizedSeries }; _scannedSeries.AddOrUpdate(existingKey, new List() {info}, (_, oldValue) => @@ -174,8 +182,11 @@ namespace API.Services.Tasks.Scanner public string MergeName(ParserInfo info) { var normalizedSeries = Parser.Parser.Normalize(info.Series); + var normalizedLocalSeries = Parser.Parser.Normalize(info.LocalizedSeries); var existingName = - _scannedSeries.SingleOrDefault(p => Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries && p.Key.Format == info.Format) + _scannedSeries.SingleOrDefault(p => + (Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries || + Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) && p.Key.Format == info.Format) .Key; if (existingName != null && !string.IsNullOrEmpty(existingName.Name)) { diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index aa969f4a4..9b17dfaa2 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -457,10 +457,14 @@ public class ScannerService : IScannerService if (existingSeries != null) continue; var s = DbFactory.Series(infos[0].Series); - if (!string.IsNullOrEmpty(infos[0].SeriesSort)) + if (!s.SortNameLocked && !string.IsNullOrEmpty(infos[0].SeriesSort)) { s.SortName = infos[0].SeriesSort; } + if (!s.LocalizedNameLocked && !string.IsNullOrEmpty(infos[0].LocalizedSeries)) + { + s.LocalizedName = infos[0].LocalizedSeries; + } s.Format = key.Format; s.LibraryId = library.Id; // We have to manually set this since we aren't adding the series to the Library's series. newSeries.Add(s); @@ -529,6 +533,13 @@ public class ScannerService : IScannerService } } + // parsedInfos[0] is not the first volume or chapter. We need to find it + var localizedSeries = parsedInfos.Select(p => p.LocalizedSeries).FirstOrDefault(p => !string.IsNullOrEmpty(p)); + if (!series.LocalizedNameLocked && !string.IsNullOrEmpty(localizedSeries)) + { + series.LocalizedName = localizedSeries; + } + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name)); UpdateSeriesMetadata(series, allPeople, allGenres, allTags, library.Type); diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 43a3ad894..0037808d0 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -9,10 +9,10 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html index 4a4151664..bf257e2d0 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html @@ -17,17 +17,20 @@ - + + + @@ -38,7 +41,7 @@

- There is no data +

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 8fd472023..e0418dfbe 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 @@ -1,6 +1,7 @@ import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core'; import { Subject } from 'rxjs'; import { FilterSettings } from 'src/app/metadata-filter/filter-settings'; +import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; import { Library } from 'src/app/_models/library'; import { Pagination } from 'src/app/_models/pagination'; import { FilterEvent, FilterItem, SeriesFilter, SortField } from 'src/app/_models/series-filter'; @@ -9,9 +10,6 @@ import { SeriesService } from 'src/app/_services/series.service'; const FILTER_PAG_REGEX = /[^0-9]/g; -const ANIMATION_SPEED = 300; - - @Component({ selector: 'app-card-detail-layout', templateUrl: './card-detail-layout.component.html', @@ -52,9 +50,12 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { private onDestory: Subject = new Subject(); - isMobile: boolean = false; - constructor(private seriesService: SeriesService) { + get Breakpoint() { + return Breakpoint; + } + + constructor(private seriesService: SeriesService, public utilityService: UtilityService) { this.filter = this.seriesService.createSeriesFilter(); } @@ -69,9 +70,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { if (this.pagination === undefined) { this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1} } - - this.isMobile = window.innerWidth <= 480; - window.onresize = () => this.isMobile = window.innerWidth <= 480; } ngOnDestroy() { diff --git a/UI/Web/src/app/events-widget/events-widget.component.ts b/UI/Web/src/app/events-widget/events-widget.component.ts index e93418ffd..4478ce4a9 100644 --- a/UI/Web/src/app/events-widget/events-widget.component.ts +++ b/UI/Web/src/app/events-widget/events-widget.component.ts @@ -83,7 +83,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { processNotificationProgressEvent(event: Message) { const message = event.payload as NotificationProgressEvent; let data; - + let index = -1; switch (event.payload.eventType) { case 'single': const values = this.singleUpdateSource.getValue(); @@ -92,20 +92,12 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { this.activeEvents += 1; break; case 'started': - data = this.progressEventsSource.getValue(); - data.push(message); + // Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then. + data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message); this.progressEventsSource.next(data); - this.activeEvents += 1; break; case 'updated': - data = this.progressEventsSource.getValue(); - const index = data.findIndex(m => m.name === message.name); - if (index < 0) { - data.push(message); - this.activeEvents += 1; - } else { - data[index] = message; - } + data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message); this.progressEventsSource.next(data); break; case 'ended': @@ -119,6 +111,18 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { } } + private mergeOrUpdate(data: NotificationProgressEvent[], message: NotificationProgressEvent) { + const index = data.findIndex(m => m.name === message.name); + // Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then. + if (index < 0) { + data.push(message); + this.activeEvents += 1; + } else { + data[index] = message; + } + return data; + } + handleUpdateAvailableClick(message: NotificationProgressEvent) { if (this.updateNotificationModalRef != null) { return; } 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 e9bc9adfe..a5680af74 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -258,12 +258,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { */ backgroundColor: string = '#FFFFFF'; + getPageUrl = (pageNum: number) => { + if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum); + return this.readerService.getPageUrl(this.chapterId, pageNum); + } private readonly onDestroy = new Subject(); - - //getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum); - get PageNumber() { return Math.max(Math.min(this.pageNum, this.maxPages - 1), 0); } @@ -1096,10 +1097,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { //console.log('cachedImages: ', this.cachedImages.arr.map(img => this.readerService.imageUrlToPageNum(img.src) + ': ' + img.complete)); } - getPageUrl(pageNum: number) { - if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum); - return this.readerService.getPageUrl(this.chapterId, pageNum); - } + loadPage() { if (!this.canvas || !this.ctx) { return; } diff --git a/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.html index 719d0e970..3ff76260e 100644 --- a/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.html +++ b/UI/Web/src/app/series-metadata-detail/series-metadata-detail.component.html @@ -9,7 +9,7 @@ {{seriesMetadata.releaseYear}} {{seriesMetadata.language}} - {{seriesMetadata.publicationStatus | publicationStatus}}