AVIF Support & Much More! (#1992)
* Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.
This commit is contained in:
parent
c1989e2819
commit
70690b747e
73 changed files with 778 additions and 566 deletions
|
|
@ -163,27 +163,26 @@ public class ImageController : BaseApiController
|
|||
/// <summary>
|
||||
/// Returns the image associated with a web-link
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="pageNum"></param>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("web-link")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = new []{"url", "apiKey"})]
|
||||
public async Task<ActionResult> GetBookmarkImage(string url, string apiKey)
|
||||
public async Task<ActionResult> GetWebLinkImage(string url, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
if (string.IsNullOrEmpty(url)) return BadRequest("Url cannot be null");
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
// Check if the domain exists
|
||||
var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url));
|
||||
var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url, encodeFormat));
|
||||
if (!_directoryService.FileSystem.File.Exists(domainFilePath))
|
||||
{
|
||||
// We need to request the favicon and save it
|
||||
try
|
||||
{
|
||||
domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory,
|
||||
await _imageService.DownloadFaviconAsync(url));
|
||||
await _imageService.DownloadFaviconAsync(url, encodeFormat));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using API.DTOs.Jobs;
|
|||
using API.DTOs.MediaErrors;
|
||||
using API.DTOs.Stats;
|
||||
using API.DTOs.Update;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
|
|
@ -119,29 +120,22 @@ public class ServerController : BaseApiController
|
|||
return Ok(await _statsService.GetServerInfo());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the scheduling of the convert bookmarks job. Only one job will run at a time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("convert-bookmarks")]
|
||||
public ActionResult ScheduleConvertBookmarks()
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true)) return Ok();
|
||||
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllBookmarkToWebP());
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the scheduling of the convert covers job. Only one job will run at a time.
|
||||
/// Triggers the scheduling of the convert media job. This will convert all media to the target encoding (except for PNG). Only one job will run at a time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("convert-covers")]
|
||||
public ActionResult ScheduleConvertCovers()
|
||||
[HttpPost("convert-media")]
|
||||
public async Task<ActionResult> ScheduleConvertCovers()
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true)) return Ok();
|
||||
BackgroundJob.Enqueue(() => _taskScheduler.CovertAllCoversToWebP());
|
||||
var encoding = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
if (encoding == EncodeFormat.PNG)
|
||||
{
|
||||
return BadRequest(
|
||||
"You cannot convert to PNG. For covers, use Refresh Covers. Bookmarks and favicons cannot be encoded back.");
|
||||
}
|
||||
BackgroundJob.Enqueue(() => _taskScheduler.CovertAllCoversToEncoding());
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -231,15 +231,9 @@ public class SettingsController : BaseApiController
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.ConvertBookmarkToWebP && updateSettingsDto.ConvertBookmarkToWebP + string.Empty != setting.Value)
|
||||
if (setting.Key == ServerSettingKey.EncodeMediaAs && updateSettingsDto.EncodeMediaAs + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.ConvertBookmarkToWebP + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.ConvertCoverToWebP && updateSettingsDto.ConvertCoverToWebP + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.ConvertCoverToWebP + string.Empty;
|
||||
setting.Value = updateSettingsDto.EncodeMediaAs + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -222,15 +222,15 @@ public class UploadController : BaseApiController
|
|||
|
||||
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0)
|
||||
{
|
||||
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
if (thumbnailSize > 0)
|
||||
{
|
||||
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
||||
filename, convertToWebP, thumbnailSize);
|
||||
filename, encodeFormat, thumbnailSize);
|
||||
}
|
||||
|
||||
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
||||
filename, convertToWebP);
|
||||
filename, encodeFormat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using API.Services;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
|
||||
namespace API.DTOs.Settings;
|
||||
|
||||
|
|
@ -47,9 +48,11 @@ public class ServerSettingDto
|
|||
/// </summary>
|
||||
public string InstallId { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// If the server should save bookmarks as WebP encoding
|
||||
/// The format that should be used when saving media for Kavita
|
||||
/// </summary>
|
||||
public bool ConvertBookmarkToWebP { get; set; }
|
||||
/// <example>This includes things like: Covers, Bookmarks, Favicons</example>
|
||||
public EncodeFormat EncodeMediaAs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of Backups before cleanup
|
||||
/// </summary>
|
||||
|
|
@ -65,10 +68,6 @@ public class ServerSettingDto
|
|||
/// <remarks>Value should be between 1 and 30</remarks>
|
||||
public int TotalLogs { get; set; }
|
||||
/// <summary>
|
||||
/// If the server should save covers as WebP encoding
|
||||
/// </summary>
|
||||
public bool ConvertCoverToWebP { get; set; }
|
||||
/// <summary>
|
||||
/// The Host name (ie Reverse proxy domain name) for the server
|
||||
/// </summary>
|
||||
public string HostName { get; set; }
|
||||
|
|
|
|||
|
|
@ -85,11 +85,6 @@ public class ServerInfoDto
|
|||
/// <remarks>Introduced in v0.5.4</remarks>
|
||||
public int TotalPeople { get; set; }
|
||||
/// <summary>
|
||||
/// Is this instance storing bookmarks as WebP
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.5.4</remarks>
|
||||
public bool StoreBookmarksAsWebP { get; set; }
|
||||
/// <summary>
|
||||
/// Number of users on this instance using Card Layout
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.5.4</remarks>
|
||||
|
|
@ -175,8 +170,8 @@ public class ServerInfoDto
|
|||
/// <remarks>Introduced in v0.7.0</remarks>
|
||||
public long TotalReadingHours { get; set; }
|
||||
/// <summary>
|
||||
/// Is the Server saving covers as WebP
|
||||
/// The encoding the server is using to save media
|
||||
/// </summary>
|
||||
/// <remarks>Added in v0.7.0</remarks>
|
||||
public bool StoreCoversAsWebP { get; set; }
|
||||
/// <remarks>Added in v0.7.3</remarks>
|
||||
public EncodeFormat EncodeMediaAs { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.7 introduced UTC dates and GMT+1 users would sometimes have dates stored as '0000-12-31 23:00:00'.
|
||||
|
|
@ -3,7 +3,7 @@ using API.Constants;
|
|||
using API.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// New role introduced in v0.5.1. Adds the role to all users.
|
||||
|
|
@ -4,7 +4,7 @@ using API.Entities;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// New role introduced in v0.6. Adds the role to all users.
|
||||
|
|
@ -4,7 +4,7 @@ using API.Entities;
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Added in v0.7.1.18
|
||||
|
|
@ -3,7 +3,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.6.0 introduced a change in how Normalization works and hence every normalized field needs to be re-calculated
|
||||
|
|
@ -3,7 +3,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.5.6 introduced Normalized Localized Name, which allows for faster lookups and less memory usage. This migration will calculate them once
|
||||
|
|
@ -4,7 +4,7 @@ using API.Services;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// New role introduced in v0.6. Calculates the Age Rating on all Reading Lists
|
||||
|
|
@ -3,7 +3,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using API.Services.Tasks;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// In v0.5.3, we removed Light and E-Ink themes. This migration will remove the themes from the DB and default anyone on
|
||||
31
API/Data/ManualMigrations/MigrateRemoveWebPSettingRows.cs
Normal file
31
API/Data/ManualMigrations/MigrateRemoveWebPSettingRows.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Entities.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Added in v0.7.2.7/v0.7.3 in which the ConvertXToWebP Setting keys were removed. This migration will remove them.
|
||||
/// </summary>
|
||||
public static class MigrateRemoveWebPSettingRows
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, ILogger<Program> logger)
|
||||
{
|
||||
logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
var key = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.ConvertBookmarkToWebP);
|
||||
var key2 = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.ConvertCoverToWebP);
|
||||
if (key == null && key2 == null)
|
||||
{
|
||||
logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - complete. Nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
unitOfWork.SettingsRepository.Remove(key);
|
||||
unitOfWork.SettingsRepository.Remove(key2);
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
|
||||
logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ using Kavita.Common.EnvironmentInfo;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
internal sealed class SeriesRelationMigrationOutput
|
||||
{
|
||||
|
|
@ -8,7 +8,7 @@ using CsvHelper;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.6.1.2 and v0.7, this imports to a temp file the existing series relationships. It is a 3 part migration.
|
||||
|
|
@ -3,7 +3,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.6.1.38 or v0.7.0,
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data;
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.6.1.8 and v0.7, this adds library ids to all User Progress to allow for easier queries against progress
|
||||
|
|
@ -177,7 +177,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserBookmark");
|
||||
b.ToTable("AppUserBookmark", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||
|
|
@ -282,7 +282,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("ThemeId");
|
||||
|
||||
b.ToTable("AppUserPreferences");
|
||||
b.ToTable("AppUserPreferences", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||
|
|
@ -332,7 +332,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserProgresses");
|
||||
b.ToTable("AppUserProgresses", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||
|
|
@ -359,7 +359,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserRating");
|
||||
b.ToTable("AppUserRating", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||
|
|
@ -484,7 +484,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("VolumeId");
|
||||
|
||||
b.ToTable("Chapter");
|
||||
b.ToTable("Chapter", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||
|
|
@ -519,7 +519,7 @@ namespace API.Data.Migrations
|
|||
b.HasIndex("Id", "Promoted")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CollectionTag");
|
||||
b.ToTable("CollectionTag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Device", b =>
|
||||
|
|
@ -565,7 +565,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("Device");
|
||||
b.ToTable("Device", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||
|
|
@ -587,7 +587,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("FolderPath");
|
||||
b.ToTable("FolderPath", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Genre", b =>
|
||||
|
|
@ -607,7 +607,7 @@ namespace API.Data.Migrations
|
|||
b.HasIndex("NormalizedTitle")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Genre");
|
||||
b.ToTable("Genre", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Library", b =>
|
||||
|
|
@ -672,7 +672,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Library");
|
||||
b.ToTable("Library", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||
|
|
@ -721,7 +721,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.ToTable("MangaFile");
|
||||
b.ToTable("MangaFile", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MediaError", b =>
|
||||
|
|
@ -756,7 +756,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MediaError");
|
||||
b.ToTable("MediaError", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
|
||||
|
|
@ -857,7 +857,7 @@ namespace API.Data.Migrations
|
|||
b.HasIndex("Id", "SeriesId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SeriesMetadata");
|
||||
b.ToTable("SeriesMetadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
||||
|
|
@ -881,7 +881,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("TargetSeriesId");
|
||||
|
||||
b.ToTable("SeriesRelation");
|
||||
b.ToTable("SeriesRelation", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person", b =>
|
||||
|
|
@ -901,7 +901,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Person");
|
||||
b.ToTable("Person", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
|
|
@ -962,7 +962,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("ReadingList");
|
||||
b.ToTable("ReadingList", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
|
||||
|
|
@ -996,7 +996,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("VolumeId");
|
||||
|
||||
b.ToTable("ReadingListItem");
|
||||
b.ToTable("ReadingListItem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
|
|
@ -1095,7 +1095,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Series");
|
||||
b.ToTable("Series", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||
|
|
@ -1112,7 +1112,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSetting");
|
||||
b.ToTable("ServerSetting", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerStatistics", b =>
|
||||
|
|
@ -1150,7 +1150,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ServerStatistics");
|
||||
b.ToTable("ServerStatistics", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SiteTheme", b =>
|
||||
|
|
@ -1188,7 +1188,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SiteTheme");
|
||||
b.ToTable("SiteTheme", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Tag", b =>
|
||||
|
|
@ -1208,7 +1208,7 @@ namespace API.Data.Migrations
|
|||
b.HasIndex("NormalizedTitle")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tag");
|
||||
b.ToTable("Tag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
|
|
@ -1260,7 +1260,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("Volume");
|
||||
b.ToTable("Volume", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserLibrary", b =>
|
||||
|
|
@ -1275,7 +1275,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("LibrariesId");
|
||||
|
||||
b.ToTable("AppUserLibrary");
|
||||
b.ToTable("AppUserLibrary", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterGenre", b =>
|
||||
|
|
@ -1290,7 +1290,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("GenresId");
|
||||
|
||||
b.ToTable("ChapterGenre");
|
||||
b.ToTable("ChapterGenre", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterPerson", b =>
|
||||
|
|
@ -1305,7 +1305,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("PeopleId");
|
||||
|
||||
b.ToTable("ChapterPerson");
|
||||
b.ToTable("ChapterPerson", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterTag", b =>
|
||||
|
|
@ -1320,7 +1320,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("ChapterTag");
|
||||
b.ToTable("ChapterTag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||
|
|
@ -1335,7 +1335,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("CollectionTagSeriesMetadata");
|
||||
b.ToTable("CollectionTagSeriesMetadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GenreSeriesMetadata", b =>
|
||||
|
|
@ -1350,7 +1350,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("GenreSeriesMetadata");
|
||||
b.ToTable("GenreSeriesMetadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||
|
|
@ -1449,7 +1449,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("PersonSeriesMetadata");
|
||||
b.ToTable("PersonSeriesMetadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SeriesMetadataTag", b =>
|
||||
|
|
@ -1464,7 +1464,7 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("SeriesMetadataTag");
|
||||
b.ToTable("SeriesMetadataTag", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.ManualMigrations;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using API.DTOs;
|
|||
using API.DTOs.Metadata;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
|
|
@ -36,7 +37,7 @@ public interface IChapterRepository
|
|||
Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
|
||||
Task<string?> GetChapterCoverImageAsync(int chapterId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<IList<Chapter>> GetAllChaptersWithNonWebPCovers();
|
||||
Task<IList<Chapter>> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format);
|
||||
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
|
||||
Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter);
|
||||
}
|
||||
|
|
@ -208,10 +209,11 @@ public class ChapterRepository : IChapterRepository
|
|||
.ToListAsync())!;
|
||||
}
|
||||
|
||||
public async Task<IList<Chapter>> GetAllChaptersWithNonWebPCovers()
|
||||
public async Task<IList<Chapter>> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format)
|
||||
{
|
||||
var extension = format.GetExtension();
|
||||
return await _context.Chapter
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using API.Data.Misc;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using AutoMapper;
|
||||
|
|
@ -34,7 +35,7 @@ public interface ICollectionTagRepository
|
|||
Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<bool> TagExists(string title);
|
||||
Task<IList<CollectionTag>> GetAllWithNonWebPCovers();
|
||||
Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
}
|
||||
public class CollectionTagRepository : ICollectionTagRepository
|
||||
{
|
||||
|
|
@ -108,10 +109,11 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public async Task<IList<CollectionTag>> GetAllWithNonWebPCovers()
|
||||
public async Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
return await _context.CollectionTag
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public interface ILibraryRepository
|
|||
Task<string?> GetLibraryCoverImageAsync(int libraryId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<IDictionary<int, LibraryType>> GetLibraryTypesForIdsAsync(IEnumerable<int> libraryIds);
|
||||
Task<IList<Library>> GetAllWithNonWebPCovers();
|
||||
Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
}
|
||||
|
||||
public class LibraryRepository : ILibraryRepository
|
||||
|
|
@ -170,10 +170,7 @@ public class LibraryRepository : ILibraryRepository
|
|||
var c = sortChar;
|
||||
var isAlpha = char.IsLetter(sortChar);
|
||||
if (!isAlpha) c = '#';
|
||||
if (!firstCharacterMap.ContainsKey(c))
|
||||
{
|
||||
firstCharacterMap[c] = 0;
|
||||
}
|
||||
firstCharacterMap.TryAdd(c, 0);
|
||||
|
||||
firstCharacterMap[c] += 1;
|
||||
}
|
||||
|
|
@ -371,10 +368,11 @@ public class LibraryRepository : ILibraryRepository
|
|||
return dict;
|
||||
}
|
||||
|
||||
public async Task<IList<Library>> GetAllWithNonWebPCovers()
|
||||
public async Task<IList<Library>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
return await _context.Library
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public interface IReadingListRepository
|
|||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<bool> ReadingListExists(string name);
|
||||
IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId);
|
||||
Task<IList<ReadingList>> GetAllWithNonWebPCovers();
|
||||
Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<IList<string>> GetFirstFourCoverImagesByReadingListId(int readingListId);
|
||||
Task<int> RemoveReadingListsWithoutSeries();
|
||||
Task<ReadingList?> GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
|
|
@ -110,10 +110,11 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.AsEnumerable();
|
||||
}
|
||||
|
||||
public async Task<IList<ReadingList>> GetAllWithNonWebPCovers()
|
||||
public async Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
return await _context.ReadingList
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Drawing;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.ManualMigrations;
|
||||
using API.Data.Misc;
|
||||
using API.Data.Scanner;
|
||||
using API.DTOs;
|
||||
|
|
@ -132,7 +133,7 @@ public interface ISeriesRepository
|
|||
Task<IDictionary<int, int>> GetLibraryIdsForSeriesAsync();
|
||||
|
||||
Task<IList<SeriesMetadataDto>> GetSeriesMetadataForIds(IEnumerable<int> seriesIds);
|
||||
Task<IList<Series>> GetAllWithNonWebPCovers(bool customOnly = true);
|
||||
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
|
|
@ -565,12 +566,14 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// Returns custom images only
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<Series>> GetAllWithNonWebPCovers(bool customOnly = true)
|
||||
public async Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat,
|
||||
bool customOnly = true)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
var prefix = ImageService.GetSeriesFormat(0).Replace("0", string.Empty);
|
||||
return await _context.Series
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage)
|
||||
&& !c.CoverImage.EndsWith(".webp")
|
||||
&& !c.CoverImage.EndsWith(extension)
|
||||
&& (!customOnly || c.CoverImage.StartsWith(prefix)))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public interface ISettingsRepository
|
|||
Task<ServerSettingDto> GetSettingsDtoAsync();
|
||||
Task<ServerSetting> GetSettingAsync(ServerSettingKey key);
|
||||
Task<IEnumerable<ServerSetting>> GetSettingsAsync();
|
||||
void Remove(ServerSetting setting);
|
||||
}
|
||||
public class SettingsRepository : ISettingsRepository
|
||||
{
|
||||
|
|
@ -32,6 +33,11 @@ public class SettingsRepository : ISettingsRepository
|
|||
_context.Entry(settings).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Remove(ServerSetting setting)
|
||||
{
|
||||
_context.Remove(setting);
|
||||
}
|
||||
|
||||
public async Task<ServerSettingDto> GetSettingsDtoAsync()
|
||||
{
|
||||
var settings = await _context.ServerSetting
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
|
|
@ -26,7 +27,7 @@ public interface IVolumeRepository
|
|||
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
|
||||
Task<IEnumerable<Volume>> GetVolumes(int seriesId);
|
||||
Task<Volume?> GetVolumeByIdAsync(int volumeId);
|
||||
Task<IList<Volume>> GetAllWithNonWebPCovers();
|
||||
Task<IList<Volume>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
}
|
||||
public class VolumeRepository : IVolumeRepository
|
||||
{
|
||||
|
|
@ -200,10 +201,11 @@ public class VolumeRepository : IVolumeRepository
|
|||
return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
|
||||
}
|
||||
|
||||
public async Task<IList<Volume>> GetAllWithNonWebPCovers()
|
||||
public async Task<IList<Volume>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
return await _context.Volume
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,12 +101,11 @@ public static class Seed
|
|||
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
||||
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
||||
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
||||
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||
new() {Key = ServerSettingKey.ConvertCoverToWebP, Value = "false"},
|
||||
new() {Key = ServerSettingKey.HostName, Value = string.Empty},
|
||||
new() {Key = ServerSettingKey.EncodeMediaAs, Value = EncodeFormat.PNG.ToString()},
|
||||
}.ToArray());
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
|
|
|
|||
13
API/Entities/Enums/EncodeFormat.cs
Normal file
13
API/Entities/Enums/EncodeFormat.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace API.Entities.Enums;
|
||||
|
||||
public enum EncodeFormat
|
||||
{
|
||||
[Description("PNG")]
|
||||
PNG = 0,
|
||||
[Description("WebP")]
|
||||
WEBP = 1,
|
||||
[Description("AVIF")]
|
||||
AVIF = 2
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.ComponentModel;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace API.Entities.Enums;
|
||||
|
||||
|
|
@ -82,6 +83,7 @@ public enum ServerSettingKey
|
|||
/// <summary>
|
||||
/// If Kavita should save bookmarks as WebP images
|
||||
/// </summary>
|
||||
[Obsolete("Use EncodeMediaAs instead")]
|
||||
[Description("ConvertBookmarkToWebP")]
|
||||
ConvertBookmarkToWebP = 14,
|
||||
/// <summary>
|
||||
|
|
@ -102,6 +104,7 @@ public enum ServerSettingKey
|
|||
/// <summary>
|
||||
/// If Kavita should save covers as WebP images
|
||||
/// </summary>
|
||||
[Obsolete("Use EncodeMediaAs instead")]
|
||||
[Description("ConvertCoverToWebP")]
|
||||
ConvertCoverToWebP = 19,
|
||||
/// <summary>
|
||||
|
|
@ -114,4 +117,11 @@ public enum ServerSettingKey
|
|||
/// </summary>
|
||||
[Description("IpAddresses")]
|
||||
IpAddresses = 21,
|
||||
/// <summary>
|
||||
/// Encode all media as PNG/WebP/AVIF/etc.
|
||||
/// </summary>
|
||||
/// <remarks>As of v0.7.3 this replaced ConvertCoverToWebP and ConvertBookmarkToWebP</remarks>
|
||||
[Description("EncodeMediaAs")]
|
||||
EncodeMediaAs = 22,
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ public static class ApplicationServiceExtensions
|
|||
services.AddScoped<IDeviceService, DeviceService>();
|
||||
services.AddScoped<IStatisticService, StatisticService>();
|
||||
services.AddScoped<IMediaErrorService, MediaErrorService>();
|
||||
services.AddScoped<IMediaConversionService, MediaConversionService>();
|
||||
|
||||
services.AddScoped<IScannerService, ScannerService>();
|
||||
services.AddScoped<IMetadataService, MetadataService>();
|
||||
|
|
|
|||
18
API/Extensions/EncodeFormatExtensions.cs
Normal file
18
API/Extensions/EncodeFormatExtensions.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Extensions;
|
||||
|
||||
public static class EncodeFormatExtensions
|
||||
{
|
||||
public static string GetExtension(this EncodeFormat encodeFormat)
|
||||
{
|
||||
return encodeFormat switch
|
||||
{
|
||||
EncodeFormat.PNG => ".png",
|
||||
EncodeFormat.WEBP => ".webp",
|
||||
EncodeFormat.AVIF => ".avif",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.DTOs.Settings;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
|
@ -51,11 +52,8 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
|
|||
case ServerSettingKey.InstallVersion:
|
||||
destination.InstallVersion = row.Value;
|
||||
break;
|
||||
case ServerSettingKey.ConvertBookmarkToWebP:
|
||||
destination.ConvertBookmarkToWebP = bool.Parse(row.Value);
|
||||
break;
|
||||
case ServerSettingKey.ConvertCoverToWebP:
|
||||
destination.ConvertCoverToWebP = bool.Parse(row.Value);
|
||||
case ServerSettingKey.EncodeMediaAs:
|
||||
destination.EncodeMediaAs = Enum.Parse<EncodeFormat>(row.Value);
|
||||
break;
|
||||
case ServerSettingKey.TotalBackups:
|
||||
destination.TotalBackups = int.Parse(row.Value);
|
||||
|
|
|
|||
|
|
@ -115,21 +115,21 @@ public static class PersonHelper
|
|||
/// For a given role and people dtos, update a series
|
||||
/// </summary>
|
||||
/// <param name="role"></param>
|
||||
/// <param name="tags"></param>
|
||||
/// <param name="people"></param>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="allTags"></param>
|
||||
/// <param name="allPeople"></param>
|
||||
/// <param name="handleAdd">This will call with an existing or new tag, but the method does not update the series Metadata</param>
|
||||
/// <param name="onModified"></param>
|
||||
public static void UpdatePeopleList(PersonRole role, ICollection<PersonDto>? tags, Series series, IReadOnlyCollection<Person> allTags,
|
||||
public static void UpdatePeopleList(PersonRole role, ICollection<PersonDto>? people, Series series, IReadOnlyCollection<Person> allPeople,
|
||||
Action<Person> handleAdd, Action onModified)
|
||||
{
|
||||
if (tags == null) return;
|
||||
if (people == null) return;
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = series.Metadata.People.Where(p => p.Role == role).ToList();
|
||||
foreach (var existing in existingTags)
|
||||
{
|
||||
if (tags.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role
|
||||
if (people.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role
|
||||
{
|
||||
// Remove tag
|
||||
series.Metadata.People.Remove(existing);
|
||||
|
|
@ -138,9 +138,9 @@ public static class PersonHelper
|
|||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tag in tags)
|
||||
foreach (var tag in people)
|
||||
{
|
||||
var existingTag = allTags.SingleOrDefault(t => t.Name == tag.Name && t.Role == tag.Role);
|
||||
var existingTag = allPeople.FirstOrDefault(t => t.Name == tag.Name && t.Role == tag.Role);
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (series.Metadata.People.Where(t => t.Role == tag.Role).All(t => t.Name != null && !t.Name.Equals(tag.Name)))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.ManualMigrations;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Logging;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Xml.Serialization;
|
||||
using API.Archive;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services.Tasks;
|
||||
using Kavita.Common;
|
||||
|
|
@ -20,7 +21,7 @@ public interface IArchiveService
|
|||
{
|
||||
void ExtractArchive(string archivePath, string extractPath);
|
||||
int GetNumberOfPagesFromArchive(string archivePath);
|
||||
string GetCoverImage(string archivePath, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format);
|
||||
bool IsValidArchive(string archivePath);
|
||||
ComicInfo? GetComicInfo(string archivePath);
|
||||
ArchiveLibrary CanOpen(string archivePath);
|
||||
|
|
@ -201,9 +202,9 @@ public class ArchiveService : IArchiveService
|
|||
/// <param name="archivePath"></param>
|
||||
/// <param name="fileName">File name to use based on context of entity.</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <param name="saveAsWebP">When saving the file, use WebP encoding instead of PNG</param>
|
||||
/// <param name="encodeFormat">When saving the file, use encoding</param>
|
||||
/// <returns></returns>
|
||||
public string GetCoverImage(string archivePath, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
if (archivePath == null || !IsValidArchive(archivePath)) return string.Empty;
|
||||
try
|
||||
|
|
@ -219,7 +220,7 @@ public class ArchiveService : IArchiveService
|
|||
var entry = archive.Entries.Single(e => e.FullName == entryName);
|
||||
|
||||
using var stream = entry.Open();
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, saveAsWebP);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
|
||||
}
|
||||
case ArchiveLibrary.SharpCompress:
|
||||
{
|
||||
|
|
@ -230,7 +231,7 @@ public class ArchiveService : IArchiveService
|
|||
var entry = archive.Entries.Single(e => e.Key == entryName);
|
||||
|
||||
using var stream = entry.OpenEntryStream();
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, saveAsWebP);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
|
||||
}
|
||||
case ArchiveLibrary.NotSupported:
|
||||
_logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath);
|
||||
|
|
@ -426,7 +427,7 @@ public class ArchiveService : IArchiveService
|
|||
{
|
||||
entry.WriteToDirectory(extractPath, new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true, // Don't flatten, let the flatterner ensure correct order of nested folders
|
||||
ExtractFullPath = true, // Don't flatten, let the flattener ensure correct order of nested folders
|
||||
Overwrite = false
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ namespace API.Services;
|
|||
public interface IBookService
|
||||
{
|
||||
int GetNumberOfPages(string filePath);
|
||||
string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat);
|
||||
ComicInfo? GetComicInfo(string filePath);
|
||||
ParserInfo? ParseInfo(string filePath);
|
||||
/// <summary>
|
||||
|
|
@ -1062,15 +1062,15 @@ public class BookService : IBookService
|
|||
/// <param name="fileFilePath"></param>
|
||||
/// <param name="fileName">Name of the new file.</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <param name="saveAsWebP">When saving the file, use WebP encoding instead of PNG</param>
|
||||
/// <param name="encodeFormat">When saving the file, use encoding</param>
|
||||
/// <returns></returns>
|
||||
public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
if (!IsValidFile(fileFilePath)) return string.Empty;
|
||||
|
||||
if (Parser.IsPdf(fileFilePath))
|
||||
{
|
||||
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, saveAsWebP);
|
||||
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, encodeFormat);
|
||||
}
|
||||
|
||||
using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions);
|
||||
|
|
@ -1085,7 +1085,7 @@ public class BookService : IBookService
|
|||
if (coverImageContent == null) return string.Empty;
|
||||
using var stream = coverImageContent.GetContentStream();
|
||||
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, saveAsWebP);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -1098,7 +1098,7 @@ public class BookService : IBookService
|
|||
}
|
||||
|
||||
|
||||
private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP)
|
||||
private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -1108,7 +1108,7 @@ public class BookService : IBookService
|
|||
using var stream = StreamManager.GetStream("BookService.GetPdfPage");
|
||||
GetPdfPage(docReader, 0, stream);
|
||||
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, saveAsWebP);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using API.Data;
|
|||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -19,9 +18,6 @@ public interface IBookmarkService
|
|||
Task<bool> BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark);
|
||||
Task<bool> RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto);
|
||||
Task<IEnumerable<string>> GetBookmarkFilesById(IEnumerable<int> bookmarkIds);
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
Task ConvertAllBookmarkToWebP();
|
||||
Task ConvertAllCoverToWebP();
|
||||
}
|
||||
|
||||
public class BookmarkService : IBookmarkService
|
||||
|
|
@ -30,17 +26,15 @@ public class BookmarkService : IBookmarkService
|
|||
private readonly ILogger<BookmarkService> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IMediaConversionService _mediaConversionService;
|
||||
|
||||
public BookmarkService(ILogger<BookmarkService> logger, IUnitOfWork unitOfWork,
|
||||
IDirectoryService directoryService, IImageService imageService, IEventHub eventHub)
|
||||
IDirectoryService directoryService, IMediaConversionService mediaConversionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
_imageService = imageService;
|
||||
_eventHub = eventHub;
|
||||
_mediaConversionService = mediaConversionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -77,21 +71,25 @@ public class BookmarkService : IBookmarkService
|
|||
/// This is a job that runs after a bookmark is saved
|
||||
/// </summary>
|
||||
/// <remarks>This must be public</remarks>
|
||||
public async Task ConvertBookmarkToWebP(int bookmarkId)
|
||||
public async Task ConvertBookmarkToEncoding(int bookmarkId)
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var convertBookmarkToWebP =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
|
||||
var encodeFormat =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
if (!convertBookmarkToWebP) return;
|
||||
if (encodeFormat == EncodeFormat.PNG)
|
||||
{
|
||||
_logger.LogError("Cannot convert media to PNG");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the bookmark still exists
|
||||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId);
|
||||
if (bookmark == null) return;
|
||||
|
||||
bookmark.FileName = await SaveAsWebP(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId));
|
||||
bookmark.FileName = await _mediaConversionService.SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat);
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
|
@ -137,10 +135,10 @@ public class BookmarkService : IBookmarkService
|
|||
_unitOfWork.UserRepository.Add(bookmark);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (settings.ConvertBookmarkToWebP)
|
||||
if (settings.EncodeMediaAs == EncodeFormat.WEBP)
|
||||
{
|
||||
// Enqueue a task to convert the bookmark to webP
|
||||
BackgroundJob.Enqueue(() => ConvertBookmarkToWebP(bookmark.Id));
|
||||
BackgroundJob.Enqueue(() => ConvertBookmarkToEncoding(bookmark.Id));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -192,198 +190,9 @@ public class BookmarkService : IBookmarkService
|
|||
b.FileName)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a long-running job that will convert all bookmarks into WebP. Do not invoke anyway except via Hangfire.
|
||||
/// </summary>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
public async Task ConvertAllBookmarkToWebP()
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started));
|
||||
var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
|
||||
.Where(b => !b.FileName.EndsWith(".webp")).ToList();
|
||||
|
||||
var count = 1F;
|
||||
foreach (var bookmark in bookmarks)
|
||||
{
|
||||
bookmark.FileName = await SaveAsWebP(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId));
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended));
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Converted bookmarks to WebP");
|
||||
}
|
||||
|
||||
/// <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 ConvertAllCoverToWebP()
|
||||
{
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of all covers to webp");
|
||||
var coverDirectory = _directoryService.CoverImageDirectory;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started));
|
||||
var chapterCovers = await _unitOfWork.ChapterRepository.GetAllChaptersWithNonWebPCovers();
|
||||
var seriesCovers = await _unitOfWork.SeriesRepository.GetAllWithNonWebPCovers();
|
||||
|
||||
var readingListCovers = await _unitOfWork.ReadingListRepository.GetAllWithNonWebPCovers();
|
||||
var libraryCovers = await _unitOfWork.LibraryRepository.GetAllWithNonWebPCovers();
|
||||
var collectionCovers = await _unitOfWork.CollectionTagRepository.GetAllWithNonWebPCovers();
|
||||
|
||||
var totalCount = chapterCovers.Count + seriesCovers.Count + readingListCovers.Count +
|
||||
libraryCovers.Count + collectionCovers.Count;
|
||||
|
||||
var count = 1F;
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of chapters");
|
||||
foreach (var chapter in chapterCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapter.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, chapter.CoverImage, coverDirectory);
|
||||
chapter.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of series");
|
||||
foreach (var series in seriesCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, series.CoverImage, coverDirectory);
|
||||
series.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of libraries");
|
||||
foreach (var library in libraryCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(library.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, library.CoverImage, coverDirectory);
|
||||
library.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of reading lists");
|
||||
foreach (var readingList in readingListCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(readingList.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, readingList.CoverImage, coverDirectory);
|
||||
readingList.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of collections");
|
||||
foreach (var collection in collectionCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(collection.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, collection.CoverImage, coverDirectory);
|
||||
collection.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.CollectionTagRepository.Update(collection);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
// Now null out all series and volumes that aren't webp or custom
|
||||
var nonCustomOrConvertedVolumeCovers = await _unitOfWork.VolumeRepository.GetAllWithNonWebPCovers();
|
||||
foreach (var volume in nonCustomOrConvertedVolumeCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(volume.CoverImage)) continue;
|
||||
volume.CoverImage = null; // We null it out so when we call Refresh Metadata it will auto update from first chapter
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
var nonCustomOrConvertedSeriesCovers = await _unitOfWork.SeriesRepository.GetAllWithNonWebPCovers(false);
|
||||
foreach (var series in nonCustomOrConvertedSeriesCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
series.CoverImage = null; // We null it out so when we call Refresh Metadata it will auto update from first chapter
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(1F, ProgressEventType.Ended));
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Converted covers to WebP");
|
||||
}
|
||||
|
||||
|
||||
/// <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>
|
||||
/// <returns></returns>
|
||||
public async Task<string> SaveAsWebP(string imageDirectory, string filename, string targetFolder)
|
||||
{
|
||||
// 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 WebP at {Target}", fullSourcePath, fullTargetDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
// Convert target file to webp then delete original target file and update bookmark
|
||||
|
||||
try
|
||||
{
|
||||
var targetFile = await _imageService.ConvertToWebP(fullSourcePath, fullTargetDirectory);
|
||||
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}", filename);
|
||||
newFilename = filename;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not convert image to WebP");
|
||||
}
|
||||
|
||||
return newFilename;
|
||||
}
|
||||
|
||||
private static string BookmarkStem(int userId, int seriesId, int chapterId)
|
||||
public static string BookmarkStem(int userId, int seriesId, int chapterId)
|
||||
{
|
||||
return Path.Join($"{userId}", $"{seriesId}", $"{chapterId}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using Flurl;
|
||||
using Flurl.Http;
|
||||
using HtmlAgilityPack;
|
||||
|
|
@ -16,49 +18,49 @@ namespace API.Services;
|
|||
public interface IImageService
|
||||
{
|
||||
void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1);
|
||||
string GetCoverImage(string path, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Thumbnail version of a base64 image
|
||||
/// </summary>
|
||||
/// <param name="encodedImage">base64 encoded image</param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="saveAsWebP">Convert and save as webp</param>
|
||||
/// <param name="encodeFormat">Convert and save as encoding format</param>
|
||||
/// <param name="thumbnailWidth">Width of thumbnail</param>
|
||||
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
||||
string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = 320);
|
||||
string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320);
|
||||
/// <summary>
|
||||
/// Writes out a thumbnail by stream input
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="outputDirectory"></param>
|
||||
/// <param name="saveAsWebP"></param>
|
||||
/// <param name="encodeFormat"></param>
|
||||
/// <returns></returns>
|
||||
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat);
|
||||
/// <summary>
|
||||
/// Writes out a thumbnail by file path input
|
||||
/// </summary>
|
||||
/// <param name="sourceFile"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="outputDirectory"></param>
|
||||
/// <param name="saveAsWebP"></param>
|
||||
/// <param name="encodeFormat"></param>
|
||||
/// <returns></returns>
|
||||
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat);
|
||||
/// <summary>
|
||||
/// Converts the passed image to webP and outputs it in the same directory
|
||||
/// Converts the passed image to encoding and outputs it in the same directory
|
||||
/// </summary>
|
||||
/// <param name="filePath">Full path to the image to convert</param>
|
||||
/// <param name="outputPath">Where to output the file</param>
|
||||
/// <returns>File of written webp image</returns>
|
||||
Task<string> ConvertToWebP(string filePath, string outputPath);
|
||||
|
||||
/// <returns>File of written encoded image</returns>
|
||||
Task<string> ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat);
|
||||
Task<bool> IsImage(string filePath);
|
||||
Task<string> DownloadFaviconAsync(string url);
|
||||
Task<string> DownloadFaviconAsync(string url, EncodeFormat encodeFormat);
|
||||
}
|
||||
|
||||
public class ImageService : IImageService
|
||||
{
|
||||
public const string Name = "BookmarkService";
|
||||
private readonly ILogger<ImageService> _logger;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
public const string ChapterCoverImageRegex = @"v\d+_c\d+";
|
||||
|
|
@ -75,6 +77,20 @@ public class ImageService : IImageService
|
|||
/// </summary>
|
||||
public const int LibraryThumbnailWidth = 32;
|
||||
|
||||
private static readonly string[] ValidIconRelations = {
|
||||
"icon",
|
||||
"apple-touch-icon",
|
||||
"apple-touch-icon-precomposed"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A mapping of urls that need to get the icon from another url, due to strangeness (like app.plex.tv loading a black icon)
|
||||
/// </summary>
|
||||
private static readonly IDictionary<string, string> FaviconUrlMapper = new Dictionary<string, string>
|
||||
{
|
||||
["https://app.plex.tv"] = "https://plex.tv"
|
||||
};
|
||||
|
||||
public ImageService(ILogger<ImageService> logger, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
|
|
@ -96,14 +112,14 @@ public class ImageService : IImageService
|
|||
}
|
||||
}
|
||||
|
||||
public string GetCoverImage(string path, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
using var thumbnail = Image.Thumbnail(path, ThumbnailWidth);
|
||||
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
|
||||
return filename;
|
||||
}
|
||||
|
|
@ -122,12 +138,12 @@ public class ImageService : IImageService
|
|||
/// <param name="stream">Stream to write to disk. Ensure this is rewinded.</param>
|
||||
/// <param name="fileName">filename to save as without extension</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <param name="saveAsWebP">Export the file as webP otherwise will default to png</param>
|
||||
/// <param name="encodeFormat">Export the file as the passed encoding</param>
|
||||
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
||||
public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth);
|
||||
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
_directoryService.ExistOrCreate(outputDirectory);
|
||||
try
|
||||
{
|
||||
|
|
@ -137,10 +153,10 @@ public class ImageService : IImageService
|
|||
return filename;
|
||||
}
|
||||
|
||||
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
using var thumbnail = Image.Thumbnail(sourceFile, ThumbnailWidth);
|
||||
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
_directoryService.ExistOrCreate(outputDirectory);
|
||||
try
|
||||
{
|
||||
|
|
@ -150,11 +166,11 @@ public class ImageService : IImageService
|
|||
return filename;
|
||||
}
|
||||
|
||||
public Task<string> ConvertToWebP(string filePath, string outputPath)
|
||||
public Task<string> ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat)
|
||||
{
|
||||
var file = _directoryService.FileSystem.FileInfo.New(filePath);
|
||||
var fileName = file.Name.Replace(file.Extension, string.Empty);
|
||||
var outputFile = Path.Join(outputPath, fileName + ".webp");
|
||||
var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension());
|
||||
|
||||
using var sourceImage = Image.NewFromFile(filePath, false, Enums.Access.SequentialUnbuffered);
|
||||
sourceImage.WriteToFile(outputFile);
|
||||
|
|
@ -183,24 +199,26 @@ public class ImageService : IImageService
|
|||
return false;
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFaviconAsync(string url)
|
||||
public async Task<string> DownloadFaviconAsync(string url, EncodeFormat encodeFormat)
|
||||
{
|
||||
// Parse the URL to get the domain (including subdomain)
|
||||
var uri = new Uri(url);
|
||||
var domain = uri.Host;
|
||||
var baseUrl = uri.Scheme + "://" + uri.Host;
|
||||
|
||||
|
||||
if (FaviconUrlMapper.TryGetValue(baseUrl, out var value))
|
||||
{
|
||||
url = value;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var validIconRelations = new[]
|
||||
{
|
||||
"icon",
|
||||
"apple-touch-icon",
|
||||
};
|
||||
var htmlContent = url.GetStringAsync().Result;
|
||||
var htmlDocument = new HtmlDocument();
|
||||
htmlDocument.LoadHtml(htmlContent);
|
||||
var pngLinks = htmlDocument.DocumentNode.Descendants("link")
|
||||
.Where(link => validIconRelations.Contains(link.GetAttributeValue("rel", string.Empty)))
|
||||
.Where(link => ValidIconRelations.Contains(link.GetAttributeValue("rel", string.Empty)))
|
||||
.Select(link => link.GetAttributeValue("href", string.Empty))
|
||||
.Where(href => href.EndsWith(".png") || href.EndsWith(".PNG"))
|
||||
.ToList();
|
||||
|
|
@ -228,9 +246,23 @@ public class ImageService : IImageService
|
|||
.GetStreamAsync();
|
||||
|
||||
// Create the destination file path
|
||||
var filename = $"{domain}.png";
|
||||
using var image = Image.PngloadStream(faviconStream);
|
||||
image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
var filename = $"{domain}{encodeFormat.GetExtension()}";
|
||||
switch (encodeFormat)
|
||||
{
|
||||
case EncodeFormat.PNG:
|
||||
image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
break;
|
||||
case EncodeFormat.WEBP:
|
||||
image.Webpsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
break;
|
||||
case EncodeFormat.AVIF:
|
||||
image.Heifsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null);
|
||||
}
|
||||
|
||||
|
||||
_logger.LogDebug("Favicon.png for {Domain} downloaded and saved successfully", domain);
|
||||
return filename;
|
||||
|
|
@ -242,14 +274,13 @@ public class ImageService : IImageService
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = ThumbnailWidth)
|
||||
public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth);
|
||||
fileName += (saveAsWebP ? ".webp" : ".png");
|
||||
fileName += encodeFormat.GetExtension();
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName));
|
||||
return fileName;
|
||||
}
|
||||
|
|
@ -309,6 +340,7 @@ public class ImageService : IImageService
|
|||
/// <returns></returns>
|
||||
public static string GetReadingListFormat(int readingListId)
|
||||
{
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
return $"readinglist{readingListId}";
|
||||
}
|
||||
|
||||
|
|
@ -322,9 +354,9 @@ public class ImageService : IImageService
|
|||
return $"thumbnail{chapterId}";
|
||||
}
|
||||
|
||||
public static string GetWebLinkFormat(string url)
|
||||
public static string GetWebLinkFormat(string url, EncodeFormat encodeFormat)
|
||||
{
|
||||
return $"{new Uri(url).Host}.png";
|
||||
return $"{new Uri(url).Host}{encodeFormat.GetExtension()}";
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
312
API/Services/MediaConversionService.cs
Normal file
312
API/Services/MediaConversionService.cs
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
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 seriesCovers = await _unitOfWork.SeriesRepository.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;
|
||||
|
||||
var count = 1F;
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of chapters");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(0, ProgressEventType.Started));
|
||||
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++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of series");
|
||||
foreach (var series in seriesCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsEncodingFormat(coverDirectory, series.CoverImage, coverDirectory, encodeFormat);
|
||||
series.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
|
||||
count++;
|
||||
}
|
||||
|
||||
_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++;
|
||||
}
|
||||
|
||||
// Now null out all series and volumes that aren't webp or custom
|
||||
var nonCustomOrConvertedVolumeCovers = await _unitOfWork.VolumeRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
|
||||
foreach (var volume in nonCustomOrConvertedVolumeCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(volume.CoverImage)) continue;
|
||||
volume.CoverImage = null; // We null it out so when we call Refresh Metadata it will auto update from first chapter
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
var nonCustomOrConvertedSeriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, false);
|
||||
foreach (var series in nonCustomOrConvertedSeriesCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
series.CoverImage = null; // We null it out so when we call Refresh Metadata it will auto update from first chapter
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
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>
|
||||
/// <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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
|
|
@ -32,7 +33,7 @@ public interface IMetadataService
|
|||
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
|
||||
|
||||
Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true);
|
||||
Task GenerateCoversForSeries(Series series, bool convertToWebP, bool forceUpdate = false);
|
||||
Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, bool forceUpdate = false);
|
||||
Task RemoveAbandonedMetadataKeys();
|
||||
}
|
||||
|
||||
|
|
@ -63,8 +64,8 @@ public class MetadataService : IMetadataService
|
|||
/// </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>
|
||||
/// <param name="convertToWebPOnWrite">Convert image to WebP when extracting the cover</param>
|
||||
private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, bool convertToWebPOnWrite)
|
||||
/// <param name="encodeFormat">Convert image to Encoding Format when extracting the cover</param>
|
||||
private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat)
|
||||
{
|
||||
var firstFile = chapter.Files.MinBy(x => x.Chapter);
|
||||
if (firstFile == null) return Task.FromResult(false);
|
||||
|
|
@ -78,7 +79,7 @@ public class MetadataService : IMetadataService
|
|||
_logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath);
|
||||
|
||||
chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath,
|
||||
ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, convertToWebPOnWrite);
|
||||
ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter));
|
||||
return Task.FromResult(true);
|
||||
|
|
@ -141,8 +142,8 @@ public class MetadataService : IMetadataService
|
|||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
/// <param name="convertToWebP"></param>
|
||||
private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, bool convertToWebP)
|
||||
/// <param name="encodeFormat"></param>
|
||||
private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat)
|
||||
{
|
||||
_logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName);
|
||||
try
|
||||
|
|
@ -155,7 +156,7 @@ public class MetadataService : IMetadataService
|
|||
var index = 0;
|
||||
foreach (var chapter in volume.Chapters)
|
||||
{
|
||||
var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, convertToWebP);
|
||||
var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat);
|
||||
// If cover was update, either the file has changed or first scan and we should force a metadata update
|
||||
UpdateChapterLastModified(chapter, forceUpdate || chapterUpdated);
|
||||
if (index == 0 && chapterUpdated)
|
||||
|
|
@ -207,7 +208,7 @@ public class MetadataService : IMetadataService
|
|||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"));
|
||||
|
||||
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++)
|
||||
{
|
||||
|
|
@ -237,7 +238,7 @@ public class MetadataService : IMetadataService
|
|||
|
||||
try
|
||||
{
|
||||
await ProcessSeriesCoverGen(series, forceUpdate, convertToWebP);
|
||||
await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -287,23 +288,23 @@ public class MetadataService : IMetadataService
|
|||
return;
|
||||
}
|
||||
|
||||
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
|
||||
await GenerateCoversForSeries(series, convertToWebP, forceUpdate);
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
await GenerateCoversForSeries(series, encodeFormat, forceUpdate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate Cover for a Series. This is used by Scan Loop and should not be invoked directly via User Interaction.
|
||||
/// </summary>
|
||||
/// <param name="series">A full Series, with metadata, chapters, etc</param>
|
||||
/// <param name="convertToWebP">When saving the file, use WebP encoding instead of PNG</param>
|
||||
/// <param name="encodeFormat">When saving the file, what encoding should be used</param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
public async Task GenerateCoversForSeries(Series series, bool convertToWebP, bool forceUpdate = false)
|
||||
public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, bool forceUpdate = false)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name));
|
||||
|
||||
await ProcessSeriesCoverGen(series, forceUpdate, convertToWebP);
|
||||
await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat);
|
||||
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
|
|
|
|||
|
|
@ -236,7 +236,6 @@ public class ReaderService : IReaderService
|
|||
|
||||
try
|
||||
{
|
||||
// TODO: Rewrite this code to just pull user object with progress for that particular appuserprogress, else create it
|
||||
var userProgress =
|
||||
await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId);
|
||||
|
||||
|
|
@ -667,15 +666,15 @@ public class ReaderService : IReaderService
|
|||
_directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, ImageService.GetThumbnailFormat(chapter.Id));
|
||||
try
|
||||
{
|
||||
var saveAsWebp =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
|
||||
var encodeFormat =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
if (!Directory.Exists(outputDirectory))
|
||||
{
|
||||
var outputtedThumbnails = cachedImages
|
||||
.Select((img, idx) =>
|
||||
_directoryService.FileSystem.Path.Join(outputDirectory,
|
||||
_imageService.WriteCoverThumbnail(img, $"{idx}", outputDirectory, saveAsWebp)))
|
||||
_imageService.WriteCoverThumbnail(img, $"{idx}", outputDirectory, encodeFormat)))
|
||||
.ToArray();
|
||||
return CacheService.GetPageFromFiles(outputtedThumbnails, pageNum);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public interface IReadingItemService
|
|||
{
|
||||
ComicInfo? GetComicInfo(string filePath);
|
||||
int GetNumberOfPages(string filePath, MangaFormat format);
|
||||
string GetCoverImage(string filePath, string fileName, MangaFormat format, bool saveAsWebP);
|
||||
string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat);
|
||||
void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1);
|
||||
ParserInfo? ParseFile(string path, string rootPath, LibraryType type);
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ public class ReadingItemService : IReadingItemService
|
|||
}
|
||||
}
|
||||
|
||||
public string GetCoverImage(string filePath, string fileName, MangaFormat format, bool saveAsWebP)
|
||||
public string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
|
|
@ -171,10 +171,10 @@ public class ReadingItemService : IReadingItemService
|
|||
|
||||
return format switch
|
||||
{
|
||||
MangaFormat.Epub => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, saveAsWebP),
|
||||
MangaFormat.Archive => _archiveService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, saveAsWebP),
|
||||
MangaFormat.Image => _imageService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, saveAsWebP),
|
||||
MangaFormat.Pdf => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, saveAsWebP),
|
||||
MangaFormat.Epub => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat),
|
||||
MangaFormat.Archive => _archiveService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat),
|
||||
MangaFormat.Image => _imageService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat),
|
||||
MangaFormat.Pdf => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ public class ReadingListService : IReadingListService
|
|||
// .Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList();
|
||||
//
|
||||
// var combinedFile = ImageService.CreateMergedImage(fullImages, _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, $"{readingListId}.png"));
|
||||
// // webp needs to be handled
|
||||
// // webp/avif needs to be handled
|
||||
// return combinedFile;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public interface ITaskScheduler
|
|||
void CancelStatsTasks();
|
||||
Task RunStatCollection();
|
||||
void ScanSiteThemes();
|
||||
Task CovertAllCoversToWebP();
|
||||
Task CovertAllCoversToEncoding();
|
||||
Task CleanupDbEntries();
|
||||
|
||||
}
|
||||
|
|
@ -50,9 +50,9 @@ public class TaskScheduler : ITaskScheduler
|
|||
private readonly IThemeService _themeService;
|
||||
private readonly IWordCountAnalyzerService _wordCountAnalyzerService;
|
||||
private readonly IStatisticService _statisticService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IMediaConversionService _mediaConversionService;
|
||||
|
||||
public static BackgroundJobServer Client => new BackgroundJobServer();
|
||||
public static BackgroundJobServer Client => new ();
|
||||
public const string ScanQueue = "scan";
|
||||
public const string DefaultQueue = "default";
|
||||
public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read";
|
||||
|
|
@ -68,12 +68,17 @@ public class TaskScheduler : ITaskScheduler
|
|||
|
||||
private static readonly Random Rnd = new Random();
|
||||
|
||||
private static readonly RecurringJobOptions RecurringJobOptions = new RecurringJobOptions()
|
||||
{
|
||||
TimeZone = TimeZoneInfo.Local
|
||||
};
|
||||
|
||||
|
||||
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
|
||||
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
|
||||
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService,
|
||||
IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService,
|
||||
IBookmarkService bookmarkService)
|
||||
IMediaConversionService mediaConversionService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_logger = logger;
|
||||
|
|
@ -87,7 +92,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
_themeService = themeService;
|
||||
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||
_statisticService = statisticService;
|
||||
_bookmarkService = bookmarkService;
|
||||
_mediaConversionService = mediaConversionService;
|
||||
}
|
||||
|
||||
public async Task ScheduleTasks()
|
||||
|
|
@ -100,28 +105,28 @@ public class TaskScheduler : ITaskScheduler
|
|||
var scanLibrarySetting = setting;
|
||||
_logger.LogDebug("Scheduling Scan Library Task for {Setting}", scanLibrarySetting);
|
||||
RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => _scannerService.ScanLibraries(false),
|
||||
() => CronConverter.ConvertToCronNotation(scanLibrarySetting), TimeZoneInfo.Local);
|
||||
() => CronConverter.ConvertToCronNotation(scanLibrarySetting), RecurringJobOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), Cron.Daily, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), Cron.Daily, RecurringJobOptions);
|
||||
}
|
||||
|
||||
setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value;
|
||||
if (setting != null)
|
||||
{
|
||||
_logger.LogDebug("Scheduling Backup Task for {Setting}", setting);
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => CronConverter.ConvertToCronNotation(setting), TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => CronConverter.ConvertToCronNotation(setting), RecurringJobOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), Cron.Weekly, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), Cron.Weekly, RecurringJobOptions);
|
||||
}
|
||||
|
||||
RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(CleanupDbTaskId, () => _cleanupService.CleanupDbEntries(), Cron.Daily, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(CleanupDbTaskId, () => _cleanupService.CleanupDbEntries(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, RecurringJobOptions);
|
||||
}
|
||||
|
||||
#region StatsTasks
|
||||
|
|
@ -137,7 +142,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
}
|
||||
|
||||
_logger.LogDebug("Scheduling stat collection daily");
|
||||
RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), RecurringJobOptions);
|
||||
}
|
||||
|
||||
public void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false)
|
||||
|
|
@ -182,10 +187,20 @@ public class TaskScheduler : ITaskScheduler
|
|||
BackgroundJob.Enqueue(() => _themeService.Scan());
|
||||
}
|
||||
|
||||
public async Task CovertAllCoversToWebP()
|
||||
/// <summary>
|
||||
/// Do not invoke this manually, always enqueue on a background thread
|
||||
/// </summary>
|
||||
public async Task CovertAllCoversToEncoding()
|
||||
{
|
||||
await _bookmarkService.ConvertAllCoverToWebP();
|
||||
_logger.LogInformation("[BookmarkService] Queuing tasks to update Series and Volume references via Cover Refresh");
|
||||
var defaultParams = Array.Empty<object>();
|
||||
if (MediaConversionService.ConversionMethods.Any(method =>
|
||||
HasAlreadyEnqueuedTask(MediaConversionService.Name, method, defaultParams, DefaultQueue, true)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _mediaConversionService.ConvertAllManagedMediaToEncodingFormat();
|
||||
_logger.LogInformation("Queuing tasks to update Series and Volume references via Cover Refresh");
|
||||
var libraryIds = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
foreach (var lib in libraryIds)
|
||||
{
|
||||
|
|
@ -200,8 +215,10 @@ public class TaskScheduler : ITaskScheduler
|
|||
public void ScheduleUpdaterTasks()
|
||||
{
|
||||
_logger.LogInformation("Scheduling Auto-Update tasks");
|
||||
// Schedule update check between noon and 6pm local time
|
||||
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Daily(Rnd.Next(12, 18)), TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Daily(Rnd.Next(5, 23)), new RecurringJobOptions()
|
||||
{
|
||||
TimeZone = TimeZoneInfo.Local
|
||||
});
|
||||
}
|
||||
|
||||
public void ScanFolder(string folderPath, TimeSpan delay)
|
||||
|
|
|
|||
|
|
@ -58,14 +58,14 @@ public class CleanupService : ICleanupService
|
|||
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
||||
public async Task Cleanup()
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToEncoding", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true) ||
|
||||
TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToEncoding", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true))
|
||||
{
|
||||
_logger.LogInformation("Cleanup put on hold as a conversion to WebP in progress");
|
||||
_logger.LogInformation("Cleanup put on hold as a media conversion in progress");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a conversion to WebP in progress"));
|
||||
MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a media conversion in progress"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public static class Parser
|
|||
private const int RegexTimeoutMs = 5000000; // 500 ms
|
||||
public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif)";
|
||||
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)";
|
||||
public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt";
|
||||
private const string BookFileExtensions = @"\.epub|\.pdf";
|
||||
private const string XmlRegexExtensions = @"\.xml";
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ public class ProcessSeries : IProcessSeries
|
|||
_logger.LogError(ex, "[ScannerService] There was an exception updating series for {SeriesName}", series.Name);
|
||||
}
|
||||
|
||||
await _metadataService.GenerateCoversForSeries(series, (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP);
|
||||
await _metadataService.GenerateCoversForSeries(series, (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs);
|
||||
EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class StatsService : IStatsService
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly DataContext _context;
|
||||
private readonly IStatisticService _statisticService;
|
||||
private const string ApiUrl = "https://stats.kavitareader.com";
|
||||
private const string ApiUrl = "http://localhost:5003";
|
||||
|
||||
public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork, DataContext context, IStatisticService statisticService)
|
||||
{
|
||||
|
|
@ -139,8 +139,7 @@ public class StatsService : IStatsService
|
|||
TotalGenres = await _unitOfWork.GenreRepository.GetCountAsync(),
|
||||
TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(),
|
||||
UsingSeriesRelationships = await GetIfUsingSeriesRelationship(),
|
||||
StoreBookmarksAsWebP = serverSettings.ConvertBookmarkToWebP,
|
||||
StoreCoversAsWebP = serverSettings.ConvertCoverToWebP,
|
||||
EncodeMediaAs = serverSettings.EncodeMediaAs,
|
||||
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
||||
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
||||
MaxChaptersInASeries = await MaxChaptersInASeries(),
|
||||
|
|
@ -292,14 +291,14 @@ public class StatsService : IStatsService
|
|||
|
||||
private IEnumerable<FileFormatDto> AllFormats()
|
||||
{
|
||||
// TODO: Rewrite this with new migration code in feature/basic-stats
|
||||
|
||||
var results = _context.MangaFile
|
||||
.AsNoTracking()
|
||||
.AsEnumerable()
|
||||
.Select(m => new FileFormatDto()
|
||||
{
|
||||
Format = m.Format,
|
||||
Extension = Path.GetExtension(m.FilePath)?.ToLowerInvariant()!
|
||||
Extension = m.Extension
|
||||
})
|
||||
.DistinctBy(f => f.Extension)
|
||||
.ToList();
|
||||
|
|
|
|||
|
|
@ -113,13 +113,13 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
|
||||
if (BuildInfo.Version < updateVersion)
|
||||
{
|
||||
_logger.LogInformation("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion);
|
||||
_logger.LogWarning("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update),
|
||||
true);
|
||||
}
|
||||
else if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development)
|
||||
{
|
||||
_logger.LogInformation("Server is up to date. Current: {CurrentVersion}", BuildInfo.Version);
|
||||
_logger.LogWarning("Server is up to date. Current: {CurrentVersion}", BuildInfo.Version);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update),
|
||||
true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,9 +77,9 @@ public class TokenService : ITokenService
|
|||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var tokenContent = tokenHandler.ReadJwtToken(request.Token);
|
||||
var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.NameId)?.Value;
|
||||
var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.Name)?.Value;
|
||||
if (string.IsNullOrEmpty(username)) return null;
|
||||
var user = await _userManager.FindByIdAsync(username);
|
||||
var user = await _userManager.FindByNameAsync(username);
|
||||
if (user == null) return null; // This forces a logout
|
||||
var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken);
|
||||
if (!validated) return null;
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ public static class MessageFactory
|
|||
return new SignalRMessage()
|
||||
{
|
||||
Name = ConvertBookmarksProgress,
|
||||
Title = "Converting Bookmarks to WebP",
|
||||
Title = "Converting Bookmarks",
|
||||
SubTitle = string.Empty,
|
||||
EventType = eventType,
|
||||
Progress = ProgressType.Determinate,
|
||||
|
|
@ -501,7 +501,7 @@ public static class MessageFactory
|
|||
return new SignalRMessage()
|
||||
{
|
||||
Name = ConvertCoversProgress,
|
||||
Title = "Converting Covers to WebP",
|
||||
Title = "Converting Covers",
|
||||
SubTitle = string.Empty,
|
||||
EventType = eventType,
|
||||
Progress = ProgressType.Determinate,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using System.Threading.RateLimiting;
|
|||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Data.ManualMigrations;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
|
|
@ -249,6 +250,9 @@ public class Startup
|
|||
// v0.7.2
|
||||
await MigrateLoginRoles.Migrate(unitOfWork, userManager, logger);
|
||||
|
||||
// v0.7.3
|
||||
await MigrateRemoveWebPSettingRows.Migrate(unitOfWork, logger);
|
||||
|
||||
// Update the version in the DB after all migrations are run
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
installVersion.Value = BuildInfo.Version.ToString();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue