mirror of
				https://github.com/Kareadita/Kavita.git
				synced 2025-11-03 19:17:05 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			323 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			323 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using System;
 | 
						|
using System.IO;
 | 
						|
using System.Linq;
 | 
						|
using System.Threading.Tasks;
 | 
						|
using API.Comparators;
 | 
						|
using API.Data;
 | 
						|
using API.Entities.Enums;
 | 
						|
using API.Extensions;
 | 
						|
using API.SignalR;
 | 
						|
using Hangfire;
 | 
						|
using Microsoft.Extensions.Logging;
 | 
						|
 | 
						|
namespace API.Services;
 | 
						|
 | 
						|
public interface IMediaConversionService
 | 
						|
{
 | 
						|
    [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
 | 
						|
    Task ConvertAllBookmarkToEncoding();
 | 
						|
    [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
 | 
						|
    Task ConvertAllCoversToEncoding();
 | 
						|
    [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
 | 
						|
    Task ConvertAllManagedMediaToEncodingFormat();
 | 
						|
 | 
						|
    Task<string> SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder,
 | 
						|
        EncodeFormat encodeFormat);
 | 
						|
}
 | 
						|
 | 
						|
public class MediaConversionService : IMediaConversionService
 | 
						|
{
 | 
						|
    public const string Name = "MediaConversionService";
 | 
						|
    public static readonly string[] ConversionMethods = {"ConvertAllBookmarkToEncoding", "ConvertAllCoversToEncoding", "ConvertAllManagedMediaToEncodingFormat"};
 | 
						|
    private readonly IUnitOfWork _unitOfWork;
 | 
						|
    private readonly IImageService _imageService;
 | 
						|
    private readonly IEventHub _eventHub;
 | 
						|
    private readonly IDirectoryService _directoryService;
 | 
						|
    private readonly ILogger<MediaConversionService> _logger;
 | 
						|
 | 
						|
    public MediaConversionService(IUnitOfWork unitOfWork, IImageService imageService, IEventHub eventHub,
 | 
						|
        IDirectoryService directoryService, ILogger<MediaConversionService> logger)
 | 
						|
    {
 | 
						|
        _unitOfWork = unitOfWork;
 | 
						|
        _imageService = imageService;
 | 
						|
        _eventHub = eventHub;
 | 
						|
        _directoryService = directoryService;
 | 
						|
        _logger = logger;
 | 
						|
    }
 | 
						|
 | 
						|
     /// <summary>
 | 
						|
    /// Converts all Kavita managed media (bookmarks, covers, favicons, etc) to the saved target encoding.
 | 
						|
    /// Do not invoke anyway except via Hangfire.
 | 
						|
    /// </summary>
 | 
						|
    /// <remarks>This is a long-running job</remarks>
 | 
						|
    /// <returns></returns>
 | 
						|
    [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
 | 
						|
    public async Task ConvertAllManagedMediaToEncodingFormat()
 | 
						|
    {
 | 
						|
        await ConvertAllBookmarkToEncoding();
 | 
						|
        await ConvertAllCoversToEncoding();
 | 
						|
        await CoverAllFaviconsToEncoding();
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// This is a long-running job that will convert all bookmarks into a format that is not PNG. Do not invoke anyway except via Hangfire.
 | 
						|
    /// </summary>
 | 
						|
    [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
 | 
						|
    public async Task ConvertAllBookmarkToEncoding()
 | 
						|
    {
 | 
						|
        var bookmarkDirectory =
 | 
						|
            (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
 | 
						|
        var encodeFormat =
 | 
						|
            (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
 | 
						|
 | 
						|
        if (encodeFormat == EncodeFormat.PNG)
 | 
						|
        {
 | 
						|
            _logger.LogError("Cannot convert media to PNG");
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
            MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started));
 | 
						|
        var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
 | 
						|
            .Where(b => !b.FileName.EndsWith(encodeFormat.GetExtension())).ToList();
 | 
						|
 | 
						|
        var count = 1F;
 | 
						|
        foreach (var bookmark in bookmarks)
 | 
						|
        {
 | 
						|
            bookmark.FileName = await SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName,
 | 
						|
                BookmarkService.BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat);
 | 
						|
            _unitOfWork.UserRepository.Update(bookmark);
 | 
						|
            await _unitOfWork.CommitAsync();
 | 
						|
            await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
                MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Updated));
 | 
						|
            count++;
 | 
						|
        }
 | 
						|
 | 
						|
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
            MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended));
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Converted bookmarks to {Format}", encodeFormat);
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// This is a long-running job that will convert all covers into WebP. Do not invoke anyway except via Hangfire.
 | 
						|
    /// </summary>
 | 
						|
    [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
 | 
						|
    public async Task ConvertAllCoversToEncoding()
 | 
						|
    {
 | 
						|
        var coverDirectory = _directoryService.CoverImageDirectory;
 | 
						|
        var encodeFormat =
 | 
						|
            (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
 | 
						|
 | 
						|
        if (encodeFormat == EncodeFormat.PNG)
 | 
						|
        {
 | 
						|
            _logger.LogError("Cannot convert media to PNG");
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of all covers to {Format}", encodeFormat);
 | 
						|
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
            MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started));
 | 
						|
 | 
						|
        var chapterCovers = await _unitOfWork.ChapterRepository.GetAllChaptersWithCoversInDifferentEncoding(encodeFormat);
 | 
						|
        var customSeriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
 | 
						|
        var seriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, false);
 | 
						|
        var nonCustomOrConvertedVolumeCovers = await _unitOfWork.VolumeRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
 | 
						|
 | 
						|
        var readingListCovers = await _unitOfWork.ReadingListRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
 | 
						|
        var libraryCovers = await _unitOfWork.LibraryRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
 | 
						|
        var collectionCovers = await _unitOfWork.CollectionTagRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
 | 
						|
 | 
						|
        var totalCount = chapterCovers.Count + seriesCovers.Count + readingListCovers.Count +
 | 
						|
                         libraryCovers.Count + collectionCovers.Count + nonCustomOrConvertedVolumeCovers.Count + customSeriesCovers.Count;
 | 
						|
 | 
						|
        var count = 1F;
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of chapters");
 | 
						|
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
            MessageFactory.ConvertCoverProgressEvent(0, ProgressEventType.Started));
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of libraries");
 | 
						|
        foreach (var library in libraryCovers)
 | 
						|
        {
 | 
						|
            if (string.IsNullOrEmpty(library.CoverImage)) continue;
 | 
						|
 | 
						|
            var newFile = await SaveAsEncodingFormat(coverDirectory, library.CoverImage, coverDirectory, encodeFormat);
 | 
						|
            library.CoverImage = Path.GetFileName(newFile);
 | 
						|
            _unitOfWork.LibraryRepository.Update(library);
 | 
						|
            await _unitOfWork.CommitAsync();
 | 
						|
            await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
                MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
 | 
						|
            count++;
 | 
						|
        }
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of reading lists");
 | 
						|
        foreach (var readingList in readingListCovers)
 | 
						|
        {
 | 
						|
            if (string.IsNullOrEmpty(readingList.CoverImage)) continue;
 | 
						|
 | 
						|
            var newFile = await SaveAsEncodingFormat(coverDirectory, readingList.CoverImage, coverDirectory, encodeFormat);
 | 
						|
            readingList.CoverImage = Path.GetFileName(newFile);
 | 
						|
            _unitOfWork.ReadingListRepository.Update(readingList);
 | 
						|
            await _unitOfWork.CommitAsync();
 | 
						|
            await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
                MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
 | 
						|
            count++;
 | 
						|
        }
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of collections");
 | 
						|
        foreach (var collection in collectionCovers)
 | 
						|
        {
 | 
						|
            if (string.IsNullOrEmpty(collection.CoverImage)) continue;
 | 
						|
 | 
						|
            var newFile = await SaveAsEncodingFormat(coverDirectory, collection.CoverImage, coverDirectory, encodeFormat);
 | 
						|
            collection.CoverImage = Path.GetFileName(newFile);
 | 
						|
            _unitOfWork.CollectionTagRepository.Update(collection);
 | 
						|
            await _unitOfWork.CommitAsync();
 | 
						|
            await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
                MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
 | 
						|
            count++;
 | 
						|
        }
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of chapters");
 | 
						|
        foreach (var chapter in chapterCovers)
 | 
						|
        {
 | 
						|
            if (string.IsNullOrEmpty(chapter.CoverImage)) continue;
 | 
						|
 | 
						|
            var newFile = await SaveAsEncodingFormat(coverDirectory, chapter.CoverImage, coverDirectory, encodeFormat);
 | 
						|
            chapter.CoverImage = Path.GetFileName(newFile);
 | 
						|
            _unitOfWork.ChapterRepository.Update(chapter);
 | 
						|
            await _unitOfWork.CommitAsync();
 | 
						|
            await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
                MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
 | 
						|
            count++;
 | 
						|
        }
 | 
						|
 | 
						|
        // Now null out all series and volumes that aren't webp or custom
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of volumes");
 | 
						|
        foreach (var volume in nonCustomOrConvertedVolumeCovers)
 | 
						|
        {
 | 
						|
            if (string.IsNullOrEmpty(volume.CoverImage)) continue;
 | 
						|
            volume.CoverImage = volume.Chapters.MinBy(x => x.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)?.CoverImage;
 | 
						|
            _unitOfWork.VolumeRepository.Update(volume);
 | 
						|
            await _unitOfWork.CommitAsync();
 | 
						|
        }
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of series");
 | 
						|
        foreach (var series in customSeriesCovers)
 | 
						|
        {
 | 
						|
            if (string.IsNullOrEmpty(series.CoverImage)) continue;
 | 
						|
 | 
						|
            var newFile = await SaveAsEncodingFormat(coverDirectory, series.CoverImage, coverDirectory, encodeFormat);
 | 
						|
            series.CoverImage = string.IsNullOrEmpty(newFile) ?
 | 
						|
                series.CoverImage.Replace(Path.GetExtension(series.CoverImage), encodeFormat.GetExtension()) : Path.GetFileName(newFile);
 | 
						|
 | 
						|
            _unitOfWork.SeriesRepository.Update(series);
 | 
						|
            await _unitOfWork.CommitAsync();
 | 
						|
            await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
                MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
 | 
						|
            count++;
 | 
						|
        }
 | 
						|
 | 
						|
        foreach (var series in seriesCovers)
 | 
						|
        {
 | 
						|
            if (string.IsNullOrEmpty(series.CoverImage)) continue;
 | 
						|
            series.CoverImage = series.GetCoverImage();
 | 
						|
            _unitOfWork.SeriesRepository.Update(series);
 | 
						|
            await _unitOfWork.CommitAsync();
 | 
						|
        }
 | 
						|
 | 
						|
        // Get all volumes and remap their covers
 | 
						|
 | 
						|
        // Get all series and remap their covers
 | 
						|
 | 
						|
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
            MessageFactory.ConvertCoverProgressEvent(1F, ProgressEventType.Ended));
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Converted covers to {Format}", encodeFormat);
 | 
						|
    }
 | 
						|
 | 
						|
    private async Task CoverAllFaviconsToEncoding()
 | 
						|
    {
 | 
						|
        var encodeFormat =
 | 
						|
            (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
 | 
						|
 | 
						|
        if (encodeFormat == EncodeFormat.PNG)
 | 
						|
        {
 | 
						|
            _logger.LogError("Cannot convert media to PNG");
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Starting conversion of favicons to {Format}", encodeFormat);
 | 
						|
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
            MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started));
 | 
						|
        var pngFavicons = _directoryService.GetFiles(_directoryService.FaviconDirectory)
 | 
						|
            .Where(b => !b.EndsWith(encodeFormat.GetExtension())).
 | 
						|
            ToList();
 | 
						|
 | 
						|
        var count = 1F;
 | 
						|
        foreach (var file in pngFavicons)
 | 
						|
        {
 | 
						|
            await SaveAsEncodingFormat(_directoryService.FaviconDirectory, _directoryService.FileSystem.FileInfo.New(file).Name, _directoryService.FaviconDirectory,
 | 
						|
                encodeFormat);
 | 
						|
            await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
                MessageFactory.ConvertBookmarksProgressEvent(count / pngFavicons.Count, ProgressEventType.Updated));
 | 
						|
            count++;
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
        await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
 | 
						|
            MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended));
 | 
						|
 | 
						|
        _logger.LogInformation("[MediaConversionService] Converted favicons to {Format}", encodeFormat);
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Converts an image file, deletes original and returns the new path back
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="imageDirectory">Full Path to where files are stored</param>
 | 
						|
    /// <param name="filename">The file to convert</param>
 | 
						|
    /// <param name="targetFolder">Full path to where files should be stored or any stem</param>
 | 
						|
    /// <param name="encodeFormat">Encoding Format</param>
 | 
						|
    /// <returns></returns>
 | 
						|
    public async Task<string> SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, EncodeFormat encodeFormat)
 | 
						|
    {
 | 
						|
        // This must be Public as it's used in via Hangfire as a background task
 | 
						|
        var fullSourcePath = _directoryService.FileSystem.Path.Join(imageDirectory, filename);
 | 
						|
        var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty);
 | 
						|
 | 
						|
        var newFilename = string.Empty;
 | 
						|
        _logger.LogDebug("Converting {Source} image into {Encoding} at {Target}", fullSourcePath, encodeFormat, fullTargetDirectory);
 | 
						|
 | 
						|
        if (!File.Exists(fullSourcePath))
 | 
						|
        {
 | 
						|
            _logger.LogError("Requested to convert {File} but it doesn't exist", fullSourcePath);
 | 
						|
            return newFilename;
 | 
						|
        }
 | 
						|
 | 
						|
        try
 | 
						|
        {
 | 
						|
            // Convert target file to format then delete original target file
 | 
						|
            try
 | 
						|
            {
 | 
						|
                var targetFile = await _imageService.ConvertToEncodingFormat(fullSourcePath, fullTargetDirectory, encodeFormat);
 | 
						|
                var targetName = new FileInfo(targetFile).Name;
 | 
						|
                newFilename = Path.Join(targetFolder, targetName);
 | 
						|
                _directoryService.DeleteFiles(new[] {fullSourcePath});
 | 
						|
            }
 | 
						|
            catch (Exception ex)
 | 
						|
            {
 | 
						|
                _logger.LogError(ex, "Could not convert image {FilePath} to {Format}", filename, encodeFormat);
 | 
						|
                newFilename = filename;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        catch (Exception ex)
 | 
						|
        {
 | 
						|
            _logger.LogError(ex, "Could not convert image to {Format}", encodeFormat);
 | 
						|
        }
 | 
						|
 | 
						|
        return newFilename;
 | 
						|
    }
 | 
						|
 | 
						|
}
 |