mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
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:
parent
1f34068662
commit
47269b4c51
@ -6,8 +6,10 @@ using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Tests.Helpers.Builders;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -117,20 +119,20 @@ public class SeriesRepositoryTests
|
||||
{
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Name = "GetFullSeriesByAnyName Manga",
|
||||
Type = LibraryType.Manga,
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath() {Path = "C:/data/manga/"}
|
||||
},
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new SeriesBuilder("The Idaten Deities Know Only Peace")
|
||||
.WithLocalizedName("Heion Sedai no Idaten-tachi")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.Build()
|
||||
}
|
||||
};
|
||||
|
||||
var s = DbFactory.Series("The Idaten Deities Know Only Peace", "Heion Sedai no Idaten-tachi");
|
||||
s.Format = MangaFormat.Archive;
|
||||
|
||||
library.Series = new List<Series>()
|
||||
{
|
||||
s,
|
||||
};
|
||||
|
||||
_unitOfWork.LibraryRepository.Add(library);
|
||||
@ -138,16 +140,18 @@ public class SeriesRepositoryTests
|
||||
}
|
||||
|
||||
|
||||
// This test case isn't ready to go
|
||||
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Archive, "", "The Idaten Deities Know Only Peace")] // Matching on localized name in DB
|
||||
[Theory]
|
||||
[InlineData("The Idaten Deities Know Only Peace", MangaFormat.Archive, "", "The Idaten Deities Know Only Peace")] // Matching on series name in DB
|
||||
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Archive, "The Idaten Deities Know Only Peace", "The Idaten Deities Know Only Peace")] // Matching on localized name in DB
|
||||
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Pdf, "", null)]
|
||||
public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected)
|
||||
{
|
||||
await ResetDb();
|
||||
await SetupSeriesData();
|
||||
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
|
||||
1, format);
|
||||
2, format, false);
|
||||
if (expected == null)
|
||||
{
|
||||
Assert.Null(series);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -26,7 +26,7 @@ public class Feed
|
||||
public FeedAuthor Author { get; set; } = new FeedAuthor()
|
||||
{
|
||||
Name = "Kavita",
|
||||
Uri = "https://kavitareader.com"
|
||||
Uri = "https://www.kavitareader.com"
|
||||
};
|
||||
|
||||
[XmlElement("totalResults", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
|
||||
|
@ -1,9 +1,12 @@
|
||||
using System.Xml.Serialization;
|
||||
using System;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace API.DTOs.OPDS;
|
||||
|
||||
public class FeedLink
|
||||
{
|
||||
[XmlIgnore]
|
||||
public bool IsPageStream { get; set; }
|
||||
/// <summary>
|
||||
/// Relation on the Link
|
||||
/// </summary>
|
||||
@ -25,6 +28,34 @@ public class FeedLink
|
||||
[XmlAttribute("count", Namespace = "http://vaemendis.net/opds-pse/ns")]
|
||||
public int TotalPages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// lastRead MUST provide the last page read for this document. The numbering starts at 1.
|
||||
/// </summary>
|
||||
[XmlAttribute("lastRead", Namespace = "http://vaemendis.net/opds-pse/ns")]
|
||||
public int LastRead { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// lastReadDate MAY provide the date of when the lastRead attribute was last updated.
|
||||
/// </summary>
|
||||
/// <remarks>Attribute MUST conform Atom's Date construct</remarks>
|
||||
[XmlAttribute("lastReadDate", Namespace = "http://vaemendis.net/opds-pse/ns")]
|
||||
public DateTime LastReadDate { get; set; }
|
||||
|
||||
public bool ShouldSerializeLastReadDate()
|
||||
{
|
||||
return IsPageStream;
|
||||
}
|
||||
|
||||
public bool ShouldSerializeLastRead()
|
||||
{
|
||||
return LastRead >= 0;
|
||||
}
|
||||
|
||||
public bool ShouldSerializeTitle()
|
||||
{
|
||||
return !string.IsNullOrEmpty(Title);
|
||||
}
|
||||
|
||||
public bool ShouldSerializeTotalPages()
|
||||
{
|
||||
return TotalPages > 0;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
|
||||
@ -19,4 +20,6 @@ public class ProgressDto
|
||||
/// on pages that combine multiple "chapters".
|
||||
/// </summary>
|
||||
public string? BookScrollId { get; set; }
|
||||
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
}
|
||||
|
@ -46,10 +46,10 @@ public class UserPreferencesDto
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string BackgroundColor { get; set; } = "#000000";
|
||||
[Required]
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Should swiping trigger pagination
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool SwipeToPaginate { get; set; }
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||
|
@ -33,6 +33,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();
|
||||
}
|
||||
public class CollectionTagRepository : ICollectionTagRepository
|
||||
{
|
||||
@ -106,6 +107,13 @@ public class CollectionTagRepository : ICollectionTagRepository
|
||||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public async Task<IList<CollectionTag>> GetAllWithNonWebPCovers()
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
||||
{
|
||||
|
||||
|
@ -51,6 +51,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();
|
||||
}
|
||||
|
||||
public class LibraryRepository : ILibraryRepository
|
||||
@ -368,4 +369,11 @@ public class LibraryRepository : ILibraryRepository
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
public async Task<IList<Library>> GetAllWithNonWebPCovers()
|
||||
{
|
||||
return await _context.Library
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ public interface IReadingListRepository
|
||||
Task<bool> ReadingListExists(string name);
|
||||
Task<List<ReadingList>> GetAllReadingListsAsync();
|
||||
IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId);
|
||||
Task<IList<ReadingList>> GetAllWithNonWebPCovers();
|
||||
}
|
||||
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
@ -106,6 +107,13 @@ public class ReadingListRepository : IReadingListRepository
|
||||
.AsEnumerable();
|
||||
}
|
||||
|
||||
public async Task<IList<ReadingList>> GetAllWithNonWebPCovers()
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public void Remove(ReadingListItem item)
|
||||
{
|
||||
_context.ReadingListItem.Remove(item);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@ -130,6 +131,7 @@ public interface ISeriesRepository
|
||||
Task<IDictionary<int, int>> GetLibraryIdsForSeriesAsync();
|
||||
|
||||
Task<IList<SeriesMetadataDto>> GetSeriesMetadataForIds(IEnumerable<int> seriesIds);
|
||||
Task<IList<Series>> GetAllWithNonWebPCovers(bool customOnly = true);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
@ -560,6 +562,21 @@ public class SeriesRepository : ISeriesRepository
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns custom images only
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<Series>> GetAllWithNonWebPCovers(bool customOnly = true)
|
||||
{
|
||||
var prefix = ImageService.GetSeriesFormat(0).Replace("0", string.Empty);
|
||||
return await _context.Series
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage)
|
||||
&& !c.CoverImage.EndsWith(".webp")
|
||||
&& (!customOnly || c.CoverImage.StartsWith(prefix)))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
|
||||
{
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
@ -1262,38 +1279,40 @@ public class SeriesRepository : ISeriesRepository
|
||||
/// <param name="format"></param>
|
||||
/// <param name="withFullIncludes">Defaults to true. This will query against all foreign keys (deep). If false, just the series will come back</param>
|
||||
/// <returns></returns>
|
||||
public Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true)
|
||||
public Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId,
|
||||
MangaFormat format, bool withFullIncludes = true)
|
||||
{
|
||||
var normalizedSeries = seriesName.ToNormalized();
|
||||
var normalizedLocalized = localizedName.ToNormalized();
|
||||
var query = _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.Where(s => s.Format == format && format != MangaFormat.Unknown)
|
||||
.Where(s => s.NormalizedName.Equals(normalizedSeries)
|
||||
|| (s.NormalizedLocalizedName == normalizedSeries)
|
||||
|| (s.OriginalName == seriesName));
|
||||
.Where(s =>
|
||||
s.NormalizedName.Equals(normalizedSeries)
|
||||
|| s.NormalizedName.Equals(normalizedLocalized)
|
||||
|
||||
if (!string.IsNullOrEmpty(normalizedLocalized))
|
||||
{
|
||||
// TODO: Apply WhereIf
|
||||
query = query.Where(s =>
|
||||
s.NormalizedName.Equals(normalizedLocalized)
|
||||
|| (s.NormalizedLocalizedName != null && s.NormalizedLocalizedName.Equals(normalizedLocalized)));
|
||||
}
|
||||
|| s.NormalizedLocalizedName.Equals(normalizedSeries)
|
||||
|| (!string.IsNullOrEmpty(normalizedLocalized) && s.NormalizedLocalizedName.Equals(normalizedLocalized))
|
||||
|
||||
|| (s.OriginalName != null && s.OriginalName.Equals(seriesName))
|
||||
);
|
||||
if (!withFullIncludes)
|
||||
{
|
||||
return query.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
return query.Include(s => s.Metadata)
|
||||
query = query.Include(s => s.Library)
|
||||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
|
||||
.Include(s => s.Library)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags)
|
||||
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(cm => cm.People)
|
||||
@ -1306,15 +1325,12 @@ public class SeriesRepository : ISeriesRepository
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(c => c.Genres)
|
||||
|
||||
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags)
|
||||
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
.AsSplitQuery();
|
||||
return query.SingleOrDefaultAsync();
|
||||
#nullable enable
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -24,6 +25,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();
|
||||
}
|
||||
public class VolumeRepository : IVolumeRepository
|
||||
{
|
||||
@ -195,6 +197,13 @@ public class VolumeRepository : IVolumeRepository
|
||||
return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
|
||||
}
|
||||
|
||||
public async Task<IList<Volume>> GetAllWithNonWebPCovers()
|
||||
{
|
||||
return await _context.Volume
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||
{
|
||||
|
@ -83,7 +83,6 @@ public static class TagHelper
|
||||
/// <remarks>Used to remove before we update/add new tags</remarks>
|
||||
/// <param name="existingTags">Existing tags on Entity</param>
|
||||
/// <param name="tags">Tags from metadata</param>
|
||||
/// <param name="isExternal">Remove external tags?</param>
|
||||
/// <param name="action">Callback which will be executed for each tag removed</param>
|
||||
public static void RemoveTags(ICollection<Tag> existingTags, IEnumerable<string> tags, Action<Tag>? action = null)
|
||||
{
|
||||
|
@ -13,14 +13,12 @@ public class ExceptionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionMiddleware> _logger;
|
||||
private readonly IHostEnvironment _env;
|
||||
|
||||
|
||||
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostEnvironment env)
|
||||
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_env = env;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
|
@ -22,8 +22,6 @@ public interface IBookmarkService
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
Task ConvertAllBookmarkToWebP();
|
||||
Task ConvertAllCoverToWebP();
|
||||
Task ConvertBookmarkToWebP(int bookmarkId);
|
||||
|
||||
}
|
||||
|
||||
public class BookmarkService : IBookmarkService
|
||||
@ -74,6 +72,31 @@ public class BookmarkService : IBookmarkService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a job that runs after a bookmark is saved
|
||||
/// </summary>
|
||||
private async Task ConvertBookmarkToWebP(int bookmarkId)
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var convertBookmarkToWebP =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
|
||||
|
||||
if (!convertBookmarkToWebP) 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));
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new entry in the AppUserBookmarks and copies an image to BookmarkDirectory.
|
||||
/// </summary>
|
||||
@ -206,14 +229,24 @@ public class BookmarkService : IBookmarkService
|
||||
[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 chapters = await _unitOfWork.ChapterRepository.GetAllChaptersWithNonWebPCovers();
|
||||
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;
|
||||
foreach (var chapter in chapters)
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of chapters");
|
||||
foreach (var chapter in chapterCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapter.CoverImage)) continue;
|
||||
|
||||
@ -222,38 +255,91 @@ public class BookmarkService : IBookmarkService
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / chapters.Count, ProgressEventType.Started));
|
||||
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>
|
||||
/// This is a job that runs after a bookmark is saved
|
||||
/// </summary>
|
||||
public async Task ConvertBookmarkToWebP(int bookmarkId)
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var convertBookmarkToWebP =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
|
||||
|
||||
if (!convertBookmarkToWebP) 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));
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an image file, deletes original and returns the new path back
|
||||
|
@ -125,6 +125,11 @@ public class ImageService : IImageService
|
||||
return Task.FromResult(outputFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs I/O to determine if the file is a valid Image
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> IsImage(string filePath)
|
||||
{
|
||||
try
|
||||
|
@ -418,7 +418,7 @@ public class ReadingListService : IReadingListService
|
||||
public async Task<CblImportSummaryDto> CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems);
|
||||
_logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user.UserName);
|
||||
_logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName);
|
||||
var importSummary = new CblImportSummaryDto()
|
||||
{
|
||||
CblName = cblReading.Name,
|
||||
|
@ -30,6 +30,7 @@ public interface ITaskScheduler
|
||||
void CancelStatsTasks();
|
||||
Task RunStatCollection();
|
||||
void ScanSiteThemes();
|
||||
Task CovertAllCoversToWebP();
|
||||
}
|
||||
public class TaskScheduler : ITaskScheduler
|
||||
{
|
||||
@ -46,6 +47,7 @@ public class TaskScheduler : ITaskScheduler
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly IWordCountAnalyzerService _wordCountAnalyzerService;
|
||||
private readonly IStatisticService _statisticService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
|
||||
public static BackgroundJobServer Client => new BackgroundJobServer();
|
||||
public const string ScanQueue = "scan";
|
||||
@ -66,7 +68,8 @@ public class TaskScheduler : ITaskScheduler
|
||||
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)
|
||||
IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService,
|
||||
IBookmarkService bookmarkService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_logger = logger;
|
||||
@ -80,6 +83,7 @@ public class TaskScheduler : ITaskScheduler
|
||||
_themeService = themeService;
|
||||
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||
_statisticService = statisticService;
|
||||
_bookmarkService = bookmarkService;
|
||||
}
|
||||
|
||||
public async Task ScheduleTasks()
|
||||
@ -174,6 +178,17 @@ public class TaskScheduler : ITaskScheduler
|
||||
BackgroundJob.Enqueue(() => _themeService.Scan());
|
||||
}
|
||||
|
||||
public async Task CovertAllCoversToWebP()
|
||||
{
|
||||
await _bookmarkService.ConvertAllCoverToWebP();
|
||||
_logger.LogInformation("[BookmarkService] Queuing tasks to update Series and Volume references via Cover Refresh");
|
||||
var libraryIds = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
foreach (var lib in libraryIds)
|
||||
{
|
||||
RefreshMetadata(lib.Id, false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateTasks
|
||||
|
@ -311,7 +311,7 @@ public class ParseScannedFiles
|
||||
.ToList();
|
||||
|
||||
|
||||
MergeLocalizedSeriesWithSeries(infos!);
|
||||
MergeLocalizedSeriesWithSeries(infos);
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
|
@ -907,7 +907,7 @@ public static class Parser
|
||||
|
||||
public static bool IsImage(string filePath)
|
||||
{
|
||||
return !filePath.StartsWith(".") && ImageRegex.IsMatch(Path.GetExtension(filePath));
|
||||
return !filePath.StartsWith('.') && ImageRegex.IsMatch(Path.GetExtension(filePath));
|
||||
}
|
||||
|
||||
public static bool IsXml(string filePath)
|
||||
|
@ -537,12 +537,18 @@ export class ActionService implements OnDestroy {
|
||||
* @param chapters? Chapters, should have id
|
||||
* @param callback Optional callback to perform actions after API completes
|
||||
*/
|
||||
deleteMultipleSeries(seriesIds: Array<Series>, callback?: VoidActionCallback) {
|
||||
async deleteMultipleSeries(seriesIds: Array<Series>, callback?: BooleanActionCallback) {
|
||||
if (!await this.confirmService.confirm('Are you sure you want to delete ' + seriesIds.length + ' series? It will not modify files on disk.')) {
|
||||
if (callback) {
|
||||
callback(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.seriesService.deleteMultipleSeries(seriesIds.map(s => s.id)).pipe(take(1)).subscribe(() => {
|
||||
this.toastr.success('Series deleted');
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -78,7 +78,8 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, () => {
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, (successful) => {
|
||||
if (!successful) return;
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
|
@ -103,7 +103,8 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, () => {
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, successful => {
|
||||
if (!successful) return;
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -95,7 +95,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, () => {
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, (successful) => {
|
||||
if (!successful) return;
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
});
|
||||
|
@ -318,7 +318,7 @@
|
||||
<div class="col-md-2 me-3">
|
||||
<form [formGroup]="seriesNameGroup">
|
||||
<div class="mb-3">
|
||||
<label for="series-name" class="form-label">Series Name</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="seriesNameFilterTooltip" role="button" tabindex="0"></i>
|
||||
<label for="series-name" class="form-label me-1">Series Name</label><i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="seriesNameFilterTooltip" role="button" tabindex="0"></i>
|
||||
<span class="visually-hidden" id="filter-series-name-help"><ng-container [ngTemplateOutlet]="seriesNameFilterTooltip"></ng-container></span>
|
||||
<ng-template #seriesNameFilterTooltip>Series name will filter against Name, Sort Name, or Localized Name</ng-template>
|
||||
<input type="text" id="series-name" formControlName="seriesNameQuery" class="form-control" aria-describedby="filter-series-name-help">
|
||||
|
@ -622,6 +622,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.readProgressGroup.get('inProgress')?.setValue(true);
|
||||
this.sortGroup.get('sortField')?.setValue(SortField.SortName);
|
||||
this.isAscendingSort = true;
|
||||
this.seriesNameGroup.get('seriesNameQuery')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
// Apply any presets which will trigger the apply
|
||||
this.loadFromPresetsAndSetup();
|
||||
|
16
openapi.json
16
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.1.12"
|
||||
"version": "0.7.1.13"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -3427,10 +3427,12 @@
|
||||
"tags": [
|
||||
"Opds"
|
||||
],
|
||||
"summary": "This returns a streamed image following OPDS-PS v1.2",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apiKey",
|
||||
"in": "path",
|
||||
"description": "",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
@ -3439,6 +3441,7 @@
|
||||
{
|
||||
"name": "libraryId",
|
||||
"in": "query",
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -3447,6 +3450,7 @@
|
||||
{
|
||||
"name": "seriesId",
|
||||
"in": "query",
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -3455,6 +3459,7 @@
|
||||
{
|
||||
"name": "volumeId",
|
||||
"in": "query",
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -3463,6 +3468,7 @@
|
||||
{
|
||||
"name": "chapterId",
|
||||
"in": "query",
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -3471,6 +3477,7 @@
|
||||
{
|
||||
"name": "pageNumber",
|
||||
"in": "query",
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -12236,6 +12243,10 @@
|
||||
"type": "string",
|
||||
"description": "For EPUB reader, this can be an optional string of the id of a part marker, to help resume reading position\r\non pages that combine multiple \"chapters\".",
|
||||
"nullable": true
|
||||
},
|
||||
"lastModifiedUtc": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@ -14993,7 +15004,8 @@
|
||||
"description": "Manga Reader Option: Background color of the reader"
|
||||
},
|
||||
"swipeToPaginate": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"description": "Manga Reader Option: Should swiping trigger pagination"
|
||||
},
|
||||
"autoCloseMenu": {
|
||||
"type": "boolean",
|
||||
|
Loading…
x
Reference in New Issue
Block a user