OPDS-PS v1.2 Support + a few bugfixes (#1869)

* Fixed up a localization lookup test case

* Refactored some webp to a unified method

* Cleaned up some code

* Expanded webp conversion for covers to all entities

* Code cleanup

* Prompt the user when they are about to delete multiple series via bulk actions

* Aligned Kavita to OPDS-PS 1.2.

* Fixed a bug where clearing metadata filter of series name didn't clear the actual field.

* Added some documentation

* Refactored how covert covers to webp works. Now we will handle all custom covers for all entities. Volumes and Series will not be touched but instead be updated via a RefreshCovers call. This will fix up the references much faster.

* Fixed up the OPDS-PS 1.2 attributes to only show on PS links
This commit is contained in:
Joe Milazzo 2023-03-09 18:41:42 -06:00 committed by GitHub
parent 1f34068662
commit 47269b4c51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 334 additions and 99 deletions

View file

@ -806,7 +806,7 @@ public class OpdsController : BaseApiController
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
// We can't not include acc link in the feed, panels doesn't work with just page streaming option. We have to block download directly
accLink,
CreatePageStreamLink(series.LibraryId,seriesId, volumeId, chapterId, mangaFile, apiKey)
await CreatePageStreamLink(series.LibraryId, seriesId, volumeId, chapterId, mangaFile, apiKey)
},
Content = new FeedEntryContent()
{
@ -818,6 +818,16 @@ public class OpdsController : BaseApiController
return entry;
}
/// <summary>
/// This returns a streamed image following OPDS-PS v1.2
/// </summary>
/// <param name="apiKey"></param>
/// <param name="libraryId"></param>
/// <param name="seriesId"></param>
/// <param name="volumeId"></param>
/// <param name="chapterId"></param>
/// <param name="pageNumber"></param>
/// <returns></returns>
[HttpGet("{apiKey}/image")]
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber)
{
@ -886,10 +896,17 @@ public class OpdsController : BaseApiController
throw new KavitaException("User does not exist");
}
private static FeedLink CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey)
private async Task<FeedLink> CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey)
{
var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg", $"{Prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}");
var userId = await GetUser(apiKey);
var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId);
var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg",
$"{Prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}");
link.TotalPages = mangaFile.Pages;
link.LastRead = progress.PageNum;
link.LastReadDate = progress.LastModifiedUtc;
link.IsPageStream = true;
return link;
}

View file

@ -103,7 +103,7 @@ public class ReaderController : BaseApiController
{
var path = _cacheService.GetCachedPagePath(chapter.Id, page);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache.");
var format = Path.GetExtension(path).Replace(".", "");
var format = Path.GetExtension(path).Replace(".", string.Empty);
return PhysicalFile(path, "image/" + format, Path.GetFileName(path), true);
}

View file

@ -22,14 +22,12 @@ public class ReadingListController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
private readonly IReadingListService _readingListService;
private readonly IDirectoryService _directoryService;
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService, IDirectoryService directoryService)
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
_readingListService = readingListService;
_directoryService = directoryService;
}
/// <summary>

View file

@ -30,14 +30,15 @@ public class ServerController : BaseApiController
private readonly IVersionUpdaterService _versionUpdaterService;
private readonly IStatsService _statsService;
private readonly ICleanupService _cleanupService;
private readonly IEmailService _emailService;
private readonly IBookmarkService _bookmarkService;
private readonly IScannerService _scannerService;
private readonly IAccountService _accountService;
private readonly ITaskScheduler _taskScheduler;
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService, IScannerService scannerService, IAccountService accountService)
ICleanupService cleanupService, IBookmarkService bookmarkService, IScannerService scannerService, IAccountService accountService,
ITaskScheduler taskScheduler)
{
_applicationLifetime = applicationLifetime;
_logger = logger;
@ -46,10 +47,10 @@ public class ServerController : BaseApiController
_versionUpdaterService = versionUpdaterService;
_statsService = statsService;
_cleanupService = cleanupService;
_emailService = emailService;
_bookmarkService = bookmarkService;
_scannerService = scannerService;
_accountService = accountService;
_taskScheduler = taskScheduler;
}
/// <summary>
@ -151,7 +152,7 @@ public class ServerController : BaseApiController
{
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
TaskScheduler.DefaultQueue, true)) return Ok();
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllCoverToWebP());
BackgroundJob.Enqueue(() => _taskScheduler.CovertAllCoversToWebP());
return Ok();
}

View file

@ -93,8 +93,7 @@ public class UploadController : BaseApiController
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
if (series == null) return BadRequest("Invalid Series");
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id), convertToWebP);
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
if (!string.IsNullOrEmpty(filePath))
{
@ -142,8 +141,7 @@ public class UploadController : BaseApiController
{
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
if (tag == null) return BadRequest("Invalid Tag id");
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}", convertToWebP);
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
if (!string.IsNullOrEmpty(filePath))
{
@ -194,8 +192,7 @@ public class UploadController : BaseApiController
{
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
if (readingList == null) return BadRequest("Reading list is not valid");
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}", convertToWebP);
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
if (!string.IsNullOrEmpty(filePath))
{
@ -222,6 +219,19 @@ public class UploadController : BaseApiController
return BadRequest("Unable to save cover image to Reading List");
}
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0)
{
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
if (thumbnailSize > 0)
{
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
filename, convertToWebP, thumbnailSize);
}
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
filename, convertToWebP); ;
}
/// <summary>
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
/// </summary>
@ -243,8 +253,7 @@ public class UploadController : BaseApiController
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
if (chapter == null) return BadRequest("Invalid Chapter");
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}", convertToWebP);
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
if (!string.IsNullOrEmpty(filePath))
{
@ -310,9 +319,7 @@ public class UploadController : BaseApiController
try
{
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", convertToWebP, ImageService.LibraryThumbnailWidth);
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", ImageService.LibraryThumbnailWidth);
if (!string.IsNullOrEmpty(filePath))
{