Estimated time is coded up.

This commit is contained in:
Joseph Milazzo 2025-07-08 15:33:10 -05:00
parent ab6669703d
commit 64ee5ee459
10 changed files with 38 additions and 51 deletions

View File

@ -10,4 +10,8 @@ namespace API.Controllers;
[Authorize]
public class BaseApiController : ControllerBase
{
public BaseApiController()
{
}
}

View File

@ -41,13 +41,12 @@ public class BookController : BaseApiController
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("{chapterId}/book-info")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "includeWordCounts"])]
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId, bool includeWordCounts = false)
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId"])]
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
{
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var bookTitle = string.Empty;
IDictionary<int, int>? pageWordCounts = null;
switch (dto.SeriesFormat)
{
@ -57,11 +56,6 @@ public class BookController : BaseApiController
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.LenientBookReaderOptions);
bookTitle = book.Title;
if (includeWordCounts)
{
// TODO: Cache this in temp/chapterId folder to avoid having to process file each time
pageWordCounts = await _bookService.GetWordCountsPerPage(mangaFile.FilePath);
}
break;
}
case MangaFormat.Pdf:
@ -94,7 +88,6 @@ public class BookController : BaseApiController
LibraryId = dto.LibraryId,
IsSpecial = dto.IsSpecial,
Pages = dto.Pages,
PageWordCounts = pageWordCounts
};

View File

@ -15,6 +15,7 @@ using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.Services.Plus;
using API.Services.Tasks.Metadata;
using API.SignalR;
using Hangfire;
using Kavita.Common;
@ -222,7 +223,6 @@ public class ReaderController : BaseApiController
/// <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="includeWordCounts">Include epub word counts per page. Only useful for epub-based reading</param>
/// <returns></returns>
[HttpGet("chapter-info")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"])]
@ -849,10 +849,18 @@ public class ReaderController : BaseApiController
// Patch in the reading progress
await _unitOfWork.ChapterRepository.AddChapterModifiers(User.GetUserId(), chapter);
// TODO: We need to actually use word count from the pages
if (series.Format == MangaFormat.Epub)
{
var progressCount = chapter.WordCount;
// 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);
}

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using API.Entities.Enums;
using API.Entities.Enums;
namespace API.DTOs.Reader;
@ -16,9 +15,4 @@ public sealed record BookInfoDto : IChapterInfoDto
public int Pages { get; set; }
public bool IsSpecial { get; set; }
public string ChapterTitle { get; set; } = default! ;
/// <summary>
/// For Epub reader, this will contain Page number -> word count. All other times will be null.
/// </summary>
/// <remarks>This is optionally returned by includeWordCounts</remarks>
public IDictionary<int, int>? PageWordCounts { get; set; }
}

View File

@ -247,7 +247,6 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
_unitOfWork.MangaFileRepository.Update(file);
}
private async Task<int> GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile, string filePath)
{
try
@ -256,7 +255,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
doc.LoadHtml(await bookFile.ReadContentAsync());
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) / AverageCharactersPerWord ?? 0;
var characterCount = textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) ?? 0;
return GetWordCount(characterCount);
}
catch (EpubContentException ex)
{
@ -267,4 +267,10 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
}
}
public static int GetWordCount(int characterCount)
{
if (characterCount == 0) return 0;
return characterCount / AverageCharactersPerWord;
}
}

View File

@ -1,7 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
import {Pipe, PipeTransform} from '@angular/core';
import {TranslocoService} from "@jsverse/transloco";
import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range";
import {DecimalPipe} from "@angular/common";
@Pipe({
name: 'readTimeLeft',
@ -11,10 +10,10 @@ export class ReadTimeLeftPipe implements PipeTransform {
constructor(private readonly translocoService: TranslocoService) {}
transform(readingTimeLeft: HourEstimateRange): string {
transform(readingTimeLeft: HourEstimateRange, includeLeftLabel = false): string {
const hoursLabel = readingTimeLeft.avgHours > 1
? this.translocoService.translate('read-time-pipe.hours')
: this.translocoService.translate('read-time-pipe.hour');
? this.translocoService.translate(`read-time-pipe.hours${includeLeftLabel ? '-left' : ''}`)
: this.translocoService.translate(`read-time-pipe.hour${includeLeftLabel ? '-left' : ''}`);
const formattedHours = this.customRound(readingTimeLeft.avgHours);

View File

@ -122,7 +122,7 @@
}
<div class="book-title col-2 d-none d-sm-block">
<div class="book-title col-3 d-none d-sm-block">
@if(isLoading) {
<!--Just render a blank div here-->
} @else {
@ -139,9 +139,9 @@
@let timeLeft = readingTimeLeftResource.value();
@if (timeLeft) {
,
<span class="time-left" [ngbTooltip]="t('time-left-alt')">
<span class="time-left">
<i class="fa-solid fa-clock" aria-hidden="true"></i>
{{timeLeft! | readTimeLeft }}
{{timeLeft! | readTimeLeft:true }}
</span>
}

View File

@ -246,12 +246,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
nextPageDisabled = false;
/**
* Internal property used to capture all the different css properties to render on all elements. This is a cached version that is updated from reader-settings component
*/
//pageStyles = model<PageStyle>(this.readerSettingsService.getDefaultPageStyles());
//pageStyles!: PageStyle;
/**
* Offset for drawer and rendering canvas. Fixed to 62px.
*/
@ -270,7 +264,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
request: () => ({
chapterId: this.chapterId,
seriesId: this.seriesId,
pageNumber: this.pageNum()
pageNumber: this.pageNum(),
}),
loader: async ({ request }) => {
return this.readerService.getTimeLeftForChapter(this.seriesId, this.chapterId).toPromise();
@ -420,16 +414,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return this.pageNum() === 0 && (currentVirtualPage === 0);
}
// get VerticalBookContentWidth() {
// if (this.layoutMode() !== BookPageLayoutMode.Default && this.writingStyle() !== WritingStyle.Horizontal ) {
// const width = this.getVerticalPageWidth()
// return width + 'px';
// }
// return '';
// }
get PageWidthForPagination() {
if (this.layoutMode() === BookPageLayoutMode.Default && this.writingStyle() === WritingStyle.Vertical && this.horizontalScrollbarNeeded) {

View File

@ -6,8 +6,4 @@ export interface BookInfo {
seriesId: number;
libraryId: number;
volumeId: number;
/**
* Maps the page number to character count. Only available on epub reader.
*/
pageWordCounts: {[key: number]: number};
}

View File

@ -2785,7 +2785,9 @@
"read-time-pipe": {
"less-than-hour": "<1 Hour",
"hour": "Hour",
"hours": "Hours"
"hours": "Hours",
"hour-left": "Hour left",
"hours-left": "Hours left"
},
"metadata-setting-field-pipe": {