mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-01 04:34:49 -04:00
# Fixed - Fixed: Clean the pdf extension from Series title for PDF types - Fixed: Fixed a bug where a forced metadata refresh wouldn't trigger a volume to force a refresh of cover image - Fixed: Fixed an issue where Removing series no longer on disk would not use the Series Format and thus after deleting files, they would not be removed. - Fixed: Fixed an issue with reading a single image file, where the cache code would not properly move the file - Fixed: For Specials, Get Next/Prev Chapter should use the filename instead of arbitrary Number (which is always 0). Use the same sorting logic when requesting volumes on series detail, so sorting can happen in the backend. # Added - Added: (Accessibility) Nearly every page has had a title set for it =============================================================================== * Clean the pdf extension from ParseSeries * Fixed a bug where forced metadata refresh wouldn't trigger the volume to update it's image. * Added titles to most pages to help distinguish back/forward history. Fixed a bug in the scanner which didn't account for Format when calculating if we need to remove a series not on disk. * For Specials, Get Next/Prev Chapter should use the filename instead of arbitrary Number (which is always 0). Use the same sorting logic when requesting volumes on series detail, so sorting can happen in the backend. * Fixed unit tests
207 lines
8.4 KiB
C#
207 lines
8.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
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 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 ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
|
public static readonly int ThumbnailWidth = 320; // 153w x 230h
|
|
|
|
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
|
|
IArchiveService archiveService, IBookService bookService, IImageService imageService)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
_logger = logger;
|
|
_archiveService = archiveService;
|
|
_bookService = bookService;
|
|
_imageService = imageService;
|
|
}
|
|
|
|
private static bool ShouldFindCoverImage(byte[] coverImage, bool forceUpdate = false)
|
|
{
|
|
return forceUpdate || coverImage == null || !coverImage.Any();
|
|
}
|
|
|
|
private byte[] GetCoverImage(MangaFile file, bool createThumbnail = true)
|
|
{
|
|
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>();
|
|
}
|
|
}
|
|
|
|
public void UpdateMetadata(Chapter chapter, bool forceUpdate)
|
|
{
|
|
var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
|
if (ShouldFindCoverImage(chapter.CoverImage, forceUpdate) && firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified))
|
|
{
|
|
chapter.Files ??= new List<MangaFile>();
|
|
chapter.CoverImage = GetCoverImage(firstFile);
|
|
}
|
|
}
|
|
|
|
|
|
public void UpdateMetadata(Volume volume, bool forceUpdate)
|
|
{
|
|
if (volume == null || !ShouldFindCoverImage(volume.CoverImage, forceUpdate)) return;
|
|
|
|
volume.Chapters ??= new List<Chapter>();
|
|
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).FirstOrDefault();
|
|
|
|
// Skip calculating Cover Image (I/O) if the chapter already has it set
|
|
if (firstChapter == null || ShouldFindCoverImage(firstChapter.CoverImage, forceUpdate))
|
|
{
|
|
var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
|
if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified))
|
|
{
|
|
volume.CoverImage = GetCoverImage(firstFile);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
volume.CoverImage = firstChapter.CoverImage;
|
|
}
|
|
}
|
|
|
|
public void UpdateMetadata(Series series, bool forceUpdate)
|
|
{
|
|
if (series == null) return;
|
|
if (ShouldFindCoverImage(series.CoverImage, forceUpdate))
|
|
{
|
|
series.Volumes ??= new List<Volume>();
|
|
var firstCover = series.Volumes.GetCoverImage(series.Library.Type);
|
|
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), _chapterSortComparer)
|
|
.FirstOrDefault(c => !c.IsSpecial)?.CoverImage;
|
|
}
|
|
|
|
if (coverImage == null)
|
|
{
|
|
coverImage = series.Volumes[0].Chapters.OrderBy(c => double.Parse(c.Number), _chapterSortComparer)
|
|
.FirstOrDefault()?.CoverImage;
|
|
}
|
|
}
|
|
series.CoverImage = firstCover?.CoverImage ?? coverImage;
|
|
}
|
|
|
|
UpdateSeriesSummary(series, forceUpdate);
|
|
}
|
|
|
|
private void UpdateSeriesSummary(Series series, bool forceUpdate)
|
|
{
|
|
if (!string.IsNullOrEmpty(series.Summary) && !forceUpdate) return;
|
|
|
|
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;
|
|
if (Parser.Parser.IsPdf(firstFile.FilePath)) return;
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
public void RefreshMetadata(int libraryId, bool forceUpdate = false)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult();
|
|
|
|
// TODO: 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)
|
|
{
|
|
foreach (var volume in series.Volumes)
|
|
{
|
|
foreach (var chapter in volume.Chapters)
|
|
{
|
|
UpdateMetadata(chapter, forceUpdate);
|
|
}
|
|
|
|
UpdateMetadata(volume, forceUpdate);
|
|
}
|
|
|
|
UpdateMetadata(series, 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);
|
|
}
|
|
}
|
|
|
|
|
|
public void RefreshMetadataForSeries(int libraryId, int seriesId)
|
|
{
|
|
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);
|
|
foreach (var volume in series.Volumes)
|
|
{
|
|
foreach (var chapter in volume.Chapters)
|
|
{
|
|
UpdateMetadata(chapter, true);
|
|
}
|
|
|
|
UpdateMetadata(volume, true);
|
|
}
|
|
|
|
UpdateMetadata(series, true);
|
|
_unitOfWork.SeriesRepository.Update(series);
|
|
|
|
|
|
if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.CommitAsync()).Result)
|
|
{
|
|
_logger.LogInformation("Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);
|
|
}
|
|
}
|
|
}
|
|
}
|