mirror of
https://github.com/Kareadita/Kavita.git
synced 2026-06-05 22:35:17 -04:00
Epub Annotation System (#4008)
Co-authored-by: Fesaa <77553571+Fesaa@users.noreply.github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
@@ -15,6 +16,8 @@ using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.Services.Tasks.Metadata;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
@@ -41,6 +44,7 @@ public class ReaderController : BaseApiController
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IBookService _bookService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(ICacheService cacheService,
|
||||
@@ -48,7 +52,8 @@ public class ReaderController : BaseApiController
|
||||
IReaderService readerService, IBookmarkService bookmarkService,
|
||||
IAccountService accountService, IEventHub eventHub,
|
||||
IScrobblingService scrobblingService,
|
||||
ILocalizationService localizationService)
|
||||
ILocalizationService localizationService,
|
||||
IBookService bookService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_unitOfWork = unitOfWork;
|
||||
@@ -59,6 +64,7 @@ public class ReaderController : BaseApiController
|
||||
_eventHub = eventHub;
|
||||
_scrobblingService = scrobblingService;
|
||||
_localizationService = localizationService;
|
||||
_bookService = bookService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,11 +224,10 @@ public class ReaderController : BaseApiController
|
||||
/// <remarks>This is generally the first call when attempting to read to allow pre-generation of assets needed for reading</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
|
||||
/// <param name="includeDimensions">Include file dimensions. Only useful for image based reading</param>
|
||||
/// <param name="includeDimensions">Include file dimensions. Only useful for image-based reading</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-info")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"
|
||||
])]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"])]
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false)
|
||||
{
|
||||
if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore
|
||||
@@ -720,26 +725,63 @@ public class ReaderController : BaseApiController
|
||||
[HttpPost("bookmark")]
|
||||
public async Task<ActionResult> BookmarkPage(BookmarkDto bookmarkDto)
|
||||
{
|
||||
// Don't let user save past total pages.
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return new UnauthorizedResult();
|
||||
try
|
||||
{
|
||||
// Don't let user save past total pages.
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||
AppUserIncludes.Bookmarks);
|
||||
if (user == null) return new UnauthorizedResult();
|
||||
|
||||
if (!await _accountService.HasBookmarkPermission(user))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
|
||||
if (!await _accountService.HasBookmarkPermission(user))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
|
||||
|
||||
var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId);
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find"));
|
||||
var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId);
|
||||
if (chapter == null || chapter.Files.Count == 0)
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find"));
|
||||
|
||||
bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page);
|
||||
var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page);
|
||||
bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page);
|
||||
|
||||
if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
|
||||
|
||||
string path;
|
||||
string? chapterTitle;
|
||||
if (Parser.IsEpub(chapter.Files.First().Extension!))
|
||||
{
|
||||
var cachedFilePath = _cacheService.GetCachedFile(chapter);
|
||||
path = await _bookService.CopyImageToTempFromBook(chapter.Id, bookmarkDto, cachedFilePath);
|
||||
|
||||
|
||||
var chapterEntity = await _unitOfWork.ChapterRepository.GetChapterAsync(bookmarkDto.ChapterId);
|
||||
if (chapterEntity == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var toc = await _bookService.GenerateTableOfContents(chapterEntity);
|
||||
chapterTitle = BookService.GetChapterTitleFromToC(toc, bookmarkDto.Page);
|
||||
}
|
||||
else
|
||||
{
|
||||
path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page);
|
||||
chapterTitle = chapter.TitleName;
|
||||
}
|
||||
|
||||
bookmarkDto.ChapterTitle = chapterTitle;
|
||||
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(path) || !await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save"));
|
||||
}
|
||||
|
||||
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when trying to create a bookmark");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes a bookmarked page for a Chapter
|
||||
/// </summary>
|
||||
@@ -826,6 +868,48 @@ public class ReaderController : BaseApiController
|
||||
return _readerService.GetTimeEstimate(0, pagesLeft, false);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// For the current user, returns an estimate on how long it would take to finish reading the chapter.
|
||||
/// </summary>
|
||||
/// <remarks>For Epubs, this does not check words inside a chapter due to overhead so may not work in all cases.</remarks>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("time-left-for-chapter")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "chapterId"])]
|
||||
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletionForChapter(int seriesId, int chapterId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
|
||||
if (series == null || chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
|
||||
// Patch in the reading progress
|
||||
await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter);
|
||||
|
||||
if (series.Format == MangaFormat.Epub)
|
||||
{
|
||||
// Get the word counts for all the pages
|
||||
var pageCounts = await _bookService.GetWordCountsPerPage(chapter.Files.First().FilePath); // TODO: Cache
|
||||
if (pageCounts == null) return _readerService.GetTimeEstimate(series.WordCount, 0, true);
|
||||
|
||||
// Sum character counts only for pages that have been read
|
||||
var totalCharactersRead = pageCounts
|
||||
.Where(kvp => kvp.Key <= chapter.PagesRead)
|
||||
.Sum(kvp => kvp.Value);
|
||||
|
||||
var progressCount = WordCountAnalyzerService.GetWordCount(totalCharactersRead);
|
||||
var wordsLeft = series.WordCount - progressCount;
|
||||
return _readerService.GetTimeEstimate(wordsLeft, 0, true);
|
||||
}
|
||||
|
||||
var pagesLeft = chapter.Pages - chapter.PagesRead;
|
||||
return _readerService.GetTimeEstimate(0, pagesLeft, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the user's personal table of contents for the given chapter
|
||||
/// </summary>
|
||||
@@ -879,6 +963,12 @@ public class ReaderController : BaseApiController
|
||||
return BadRequest(await _localizationService.Translate(userId, "duplicate-bookmark"));
|
||||
}
|
||||
|
||||
// Look up the chapter this PTOC is associated with to get the chapter title (if there is one)
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId);
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist"));
|
||||
var toc = await _bookService.GenerateTableOfContents(chapter);
|
||||
var chapterTitle = BookService.GetChapterTitleFromToC(toc, dto.PageNumber);
|
||||
|
||||
_unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent()
|
||||
{
|
||||
Title = dto.Title.Trim(),
|
||||
@@ -887,12 +977,16 @@ public class ReaderController : BaseApiController
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
BookScrollId = dto.BookScrollId,
|
||||
SelectedText = dto.SelectedText,
|
||||
ChapterTitle = chapterTitle,
|
||||
AppUserId = userId
|
||||
});
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get all progress events for a given chapter
|
||||
/// </summary>
|
||||
@@ -905,4 +999,5 @@ public class ReaderController : BaseApiController
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.GetUserProgressForChapter(chapterId, userId));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user