mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-31 04:04:19 -04:00
* Added parser case for "The Duke of Death and His Black Maid - Ch. 177 - The Ball (3).cbz" * Removed a file that is created and modified every test run. * Fixed a bad parser case for "Batman Beyond 02 (of 6) (1999)" which was consuming too many characters * Removed a lot of "Volume" parsing for Comics that don't make sense. This is prep work for the upcoming Comic Rework release. * Reworked a lot of parsing cases for comics based on naming conventions observed from releases found online. * Added a way for external scripts to use a user api key to authenticate * Fixed an issue if the manga only had one page, the bottom menu would be missing page and chapter controls. * Fixed a bug where on small phones, nav bar could overflow due to scroll to top * Tweaked a lot of regex for manga parsing to handle some cases where poorly named files, like "Vol. 03 Ch. 21" would end up parsing as Series "Vol. 03". * Even more handling of parser cases. Manga parser should be as it was but more robust to handle bad naming. * Fixed: Don't force metadata refresh on Scan Series, only on refresh metadata * Implemented the ability to automatically refresh after a series scan based on when server finishes. Remove a duplicate API call from series detail. * Removed another API call for series metadata that isn't needed. * Refactored Message creation to a factory, hardcoded strings are centralized, and RefreshSeriesMetadata sends an event and is refactored to be async. * Fixed a bug when really poorly named files are within a folder that contains the series name, fallback couldn't occur due to it being taken as root folder. Now we detect said condition and will go one level higher, resulting in potentially more I/O, but the series will not be deleted. * Added the Read in Incognito context item for Chapter cards * Skip an additional check for series summary for series that aren't EPUB or Archive formats. * Fixed an issue where cover image generation could occur due to a bad check on LastWriteTime on the underlying file. * Added some extra comic parser tests * Added a ScanLibrary event (not hooked up in UI) * Performance improvement on metadata service. Now when we scan for cover image changes, we emit when a change occurs and only then do we update parent entities (array copy). * Removed an hr from series detail and ensure we update the cover image for series when scan series finishes. * Updated the infinite scroller to use a Flags pattern for the debug mode. Updated a few logical conditions for mobile. * Removed the concurrency check on row progress as if too many calls hit the DB, it will throw, but it doesn't matter. Fixed a bad logic code which could cause scrolling after hitting the bottom of the chapter. * Ensure prefetching uses totalPages + 1 since we pass in totalPages as - 1 from manga reader * Fixed issue where last page of webtoon wouldn't be prefetched due to a < instead of <= on prefetching code * Implemented ability to send images from archives to the UI without incurring any extra memory pressure. * Dropdown menus now have a darker background * Webtoon reader now works on mobile. * Fixed how keyboard presses for up/down/left/right work with MANGA_UD reading mode. See issue #579 * Fixed cont reader for webtoons on mobile * Fixed a small issue where top spacer would too quickly switch to prev chapter * Updated user preferences to use same slider style. Removed some css that is not used. * Added comic parser case for "Saga 001 (2012) (Digital) (Empire-Zone)" * Added accessibility toggle to reading list order and aligned sliders to all use the same style. * Removed a todo for checking on new image serving code. It works great. * Fixed a missing await * Auth guard will now check if an existing toast is present giving same message before poping the toast. * Fixed alignment on phones for reading lists * Moved sorters so they aren't resused between multiple threads. Slightly higher memory footprint. * Fixed a broken unit test * Code smells * More unit test fixing
264 lines
11 KiB
C#
264 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using API.Comparators;
|
|
using API.Entities;
|
|
using API.Entities.Enums;
|
|
using API.Extensions;
|
|
using API.Interfaces;
|
|
using API.Interfaces.Services;
|
|
using API.SignalR;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Services
|
|
{
|
|
public class MetadataService : IMetadataService
|
|
{
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly ILogger<MetadataService> _logger;
|
|
private readonly IArchiveService _archiveService;
|
|
private readonly IBookService _bookService;
|
|
private readonly IImageService _imageService;
|
|
private readonly IHubContext<MessageHub> _messageHub;
|
|
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
|
/// <summary>
|
|
/// Width of the Thumbnail generation
|
|
/// </summary>
|
|
public static readonly int ThumbnailWidth = 320; // 153w x 230h
|
|
|
|
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
|
|
IArchiveService archiveService, IBookService bookService, IImageService imageService, IHubContext<MessageHub> messageHub)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
_logger = logger;
|
|
_archiveService = archiveService;
|
|
_bookService = bookService;
|
|
_imageService = imageService;
|
|
_messageHub = messageHub;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether an entity should regenerate cover image
|
|
/// </summary>
|
|
/// <param name="coverImage"></param>
|
|
/// <param name="firstFile"></param>
|
|
/// <param name="forceUpdate"></param>
|
|
/// <param name="isCoverLocked"></param>
|
|
/// <returns></returns>
|
|
public static bool ShouldUpdateCoverImage(byte[] coverImage, MangaFile firstFile, bool forceUpdate = false,
|
|
bool isCoverLocked = false)
|
|
{
|
|
if (isCoverLocked) return false;
|
|
if (forceUpdate) return true;
|
|
return (firstFile != null && firstFile.HasFileBeenModified()) || !HasCoverImage(coverImage);
|
|
}
|
|
|
|
private static bool HasCoverImage(byte[] coverImage)
|
|
{
|
|
return coverImage != null && coverImage.Any();
|
|
}
|
|
|
|
private byte[] GetCoverImage(MangaFile file, bool createThumbnail = true)
|
|
{
|
|
file.LastModified = DateTime.Now;
|
|
switch (file.Format)
|
|
{
|
|
case MangaFormat.Pdf:
|
|
case MangaFormat.Epub:
|
|
return _bookService.GetCoverImage(file.FilePath, createThumbnail);
|
|
case MangaFormat.Image:
|
|
var coverImage = _imageService.GetCoverFile(file);
|
|
return _imageService.GetCoverImage(coverImage, createThumbnail);
|
|
case MangaFormat.Archive:
|
|
return _archiveService.GetCoverImage(file.FilePath, createThumbnail);
|
|
default:
|
|
return Array.Empty<byte>();
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the metadata for a Chapter
|
|
/// </summary>
|
|
/// <param name="chapter"></param>
|
|
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
|
public bool UpdateMetadata(Chapter chapter, bool forceUpdate)
|
|
{
|
|
var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
|
|
|
if (ShouldUpdateCoverImage(chapter.CoverImage, firstFile, forceUpdate, chapter.CoverImageLocked))
|
|
{
|
|
chapter.CoverImage = GetCoverImage(firstFile);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the metadata for a Volume
|
|
/// </summary>
|
|
/// <param name="volume"></param>
|
|
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
|
public bool UpdateMetadata(Volume volume, bool forceUpdate)
|
|
{
|
|
// We need to check if Volume coverImage matches first chapters if forceUpdate is false
|
|
if (volume == null || !ShouldUpdateCoverImage(volume.CoverImage, null, forceUpdate
|
|
, false)) return false;
|
|
|
|
volume.Chapters ??= new List<Chapter>();
|
|
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).FirstOrDefault();
|
|
if (firstChapter == null) return false;
|
|
|
|
volume.CoverImage = firstChapter.CoverImage;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates metadata for Series
|
|
/// </summary>
|
|
/// <param name="series"></param>
|
|
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
|
public bool UpdateMetadata(Series series, bool forceUpdate)
|
|
{
|
|
var madeUpdate = false;
|
|
if (series == null) return false;
|
|
if (ShouldUpdateCoverImage(series.CoverImage, null, forceUpdate, series.CoverImageLocked))
|
|
{
|
|
series.Volumes ??= new List<Volume>();
|
|
var firstCover = series.Volumes.GetCoverImage(series.Format);
|
|
byte[] coverImage = null;
|
|
if (firstCover == null && series.Volumes.Any())
|
|
{
|
|
// If firstCover is null and one volume, the whole series is Chapters under Vol 0.
|
|
if (series.Volumes.Count == 1)
|
|
{
|
|
coverImage = series.Volumes[0].Chapters.OrderBy(c => double.Parse(c.Number), _chapterSortComparerForInChapterSorting)
|
|
.FirstOrDefault(c => !c.IsSpecial)?.CoverImage;
|
|
madeUpdate = true;
|
|
}
|
|
|
|
if (!HasCoverImage(coverImage))
|
|
{
|
|
coverImage = series.Volumes[0].Chapters.OrderBy(c => double.Parse(c.Number), _chapterSortComparerForInChapterSorting)
|
|
.FirstOrDefault()?.CoverImage;
|
|
madeUpdate = true;
|
|
}
|
|
}
|
|
series.CoverImage = firstCover?.CoverImage ?? coverImage;
|
|
}
|
|
|
|
return UpdateSeriesSummary(series, forceUpdate) || madeUpdate ;
|
|
}
|
|
|
|
private bool UpdateSeriesSummary(Series series, bool forceUpdate)
|
|
{
|
|
if (!string.IsNullOrEmpty(series.Summary) && !forceUpdate) return false;
|
|
|
|
var isBook = series.Library.Type == LibraryType.Book;
|
|
var firstVolume = series.Volumes.FirstWithChapters(isBook);
|
|
var firstChapter = firstVolume?.Chapters.GetFirstChapterWithFiles();
|
|
|
|
var firstFile = firstChapter?.Files.FirstOrDefault();
|
|
if (firstFile == null || (!forceUpdate && !firstFile.HasFileBeenModified())) return false;
|
|
if (Parser.Parser.IsPdf(firstFile.FilePath)) return false;
|
|
|
|
if (series.Format is MangaFormat.Archive or MangaFormat.Epub)
|
|
{
|
|
var summary = Parser.Parser.IsEpub(firstFile.FilePath) ? _bookService.GetSummaryInfo(firstFile.FilePath) : _archiveService.GetSummaryInfo(firstFile.FilePath);
|
|
if (!string.IsNullOrEmpty(series.Summary))
|
|
{
|
|
series.Summary = summary;
|
|
firstFile.LastModified = DateTime.Now;
|
|
return true;
|
|
}
|
|
}
|
|
firstFile.LastModified = DateTime.Now; // NOTE: Should I put this here as well since it might not have actually been parsed?
|
|
return false;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Refreshes Metadata for a whole library
|
|
/// </summary>
|
|
/// <remarks>This can be heavy on memory first run</remarks>
|
|
/// <param name="libraryId"></param>
|
|
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
|
public void RefreshMetadata(int libraryId, bool forceUpdate = false)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult();
|
|
|
|
// PERF: See if we can break this up into multiple threads that process 20 series at a time then save so we can reduce amount of memory used
|
|
_logger.LogInformation("Beginning metadata refresh of {LibraryName}", library.Name);
|
|
foreach (var series in library.Series)
|
|
{
|
|
var volumeUpdated = false;
|
|
foreach (var volume in series.Volumes)
|
|
{
|
|
var chapterUpdated = false;
|
|
foreach (var chapter in volume.Chapters)
|
|
{
|
|
chapterUpdated = UpdateMetadata(chapter, forceUpdate);
|
|
}
|
|
|
|
volumeUpdated = UpdateMetadata(volume, chapterUpdated || forceUpdate);
|
|
}
|
|
|
|
UpdateMetadata(series, volumeUpdated || forceUpdate);
|
|
_unitOfWork.SeriesRepository.Update(series);
|
|
}
|
|
|
|
|
|
if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.CommitAsync()).Result)
|
|
{
|
|
_logger.LogInformation("Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Refreshes Metadata for a Series. Will always force updates.
|
|
/// </summary>
|
|
/// <param name="libraryId"></param>
|
|
/// <param name="seriesId"></param>
|
|
public async Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = false)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult();
|
|
|
|
var series = library.Series.SingleOrDefault(s => s.Id == seriesId);
|
|
if (series == null)
|
|
{
|
|
_logger.LogError("Series {SeriesId} was not found on Library {LibraryName}", seriesId, libraryId);
|
|
return;
|
|
}
|
|
_logger.LogInformation("Beginning metadata refresh of {SeriesName}", series.Name);
|
|
var volumeUpdated = false;
|
|
foreach (var volume in series.Volumes)
|
|
{
|
|
var chapterUpdated = false;
|
|
foreach (var chapter in volume.Chapters)
|
|
{
|
|
chapterUpdated = UpdateMetadata(chapter, forceUpdate);
|
|
}
|
|
|
|
volumeUpdated = UpdateMetadata(volume, chapterUpdated || forceUpdate);
|
|
}
|
|
|
|
UpdateMetadata(series, volumeUpdated || forceUpdate);
|
|
_unitOfWork.SeriesRepository.Update(series);
|
|
|
|
|
|
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
|
{
|
|
_logger.LogInformation("Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);
|
|
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.RefreshMetadataEvent(libraryId, seriesId));
|
|
}
|
|
}
|
|
}
|
|
}
|