mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Time Estimation Cleanup (#1301)
* Moved the calculation for time to read to the backend. Tweaked some logic around showing est time to complete. * Added debug logging to help pinpoint a duplicate issue in Kavita. * More combination logic is error checked in a special way for Robbie to reproduce an issue. * Migrated chapter detail card to use backend for time calculation. Ensure we take all chapters into account for volume time calcs * Tweaked messaging for some critical logs to include file * Ensure pages count uses comma separated number * Moved Hangfire annotations to interface level. Adjusted word count service to always recalculate when user requests via analyze series files.
This commit is contained in:
parent
85b4ad0c58
commit
8e69b6cfc0
@ -628,6 +628,66 @@ namespace API.Controllers
|
||||
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Given word count, page count, and if the entity is an epub file, this will return the read time.
|
||||
/// </summary>
|
||||
/// <param name="wordCount"></param>
|
||||
/// <param name="pageCount"></param>
|
||||
/// <param name="isEpub"></param>
|
||||
/// <returns>Will always assume no progress as it's not privy</returns>
|
||||
[HttpGet("manual-read-time")]
|
||||
public ActionResult<HourEstimateRangeDto> GetManualReadTime(int wordCount, int pageCount, bool isEpub)
|
||||
{
|
||||
|
||||
if (isEpub)
|
||||
{
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((wordCount / ReaderService.MinWordsPerHour)),
|
||||
MaxHours = (int) Math.Round((wordCount / ReaderService.MaxWordsPerHour)),
|
||||
AvgHours = (int) Math.Round((wordCount / ReaderService.AvgWordsPerHour)),
|
||||
HasProgress = false
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((pageCount / ReaderService.MinPagesPerMinute / 60F)),
|
||||
MaxHours = (int) Math.Round((pageCount / ReaderService.MaxPagesPerMinute / 60F)),
|
||||
AvgHours = (int) Math.Round((pageCount / ReaderService.AvgPagesPerMinute / 60F)),
|
||||
HasProgress = false
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("read-time")]
|
||||
public async Task<ActionResult<HourEstimateRangeDto>> GetReadTime(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
|
||||
var progress = (await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId)).ToList();
|
||||
if (series.Format == MangaFormat.Epub)
|
||||
{
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((series.WordCount / ReaderService.MinWordsPerHour)),
|
||||
MaxHours = (int) Math.Round((series.WordCount / ReaderService.MaxWordsPerHour)),
|
||||
AvgHours = (int) Math.Round((series.WordCount / ReaderService.AvgWordsPerHour)),
|
||||
HasProgress = progress.Any()
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((series.Pages / ReaderService.MinPagesPerMinute / 60F)),
|
||||
MaxHours = (int) Math.Round((series.Pages / ReaderService.MaxPagesPerMinute / 60F)),
|
||||
AvgHours = (int) Math.Round((series.Pages / ReaderService.AvgPagesPerMinute / 60F)),
|
||||
HasProgress = progress.Any()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// For the current user, returns an estimate on how long it would take to finish reading the series.
|
||||
/// </summary>
|
||||
|
@ -19,6 +19,7 @@ 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();
|
||||
|
||||
}
|
||||
@ -173,7 +174,6 @@ public class BookmarkService : IBookmarkService
|
||||
/// <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 =
|
||||
|
@ -27,6 +27,8 @@ public interface IMetadataService
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
Task RefreshMetadata(int libraryId, bool forceUpdate = false);
|
||||
/// <summary>
|
||||
/// Performs a forced refresh of metadata just for a series and it's nested entities
|
||||
@ -196,8 +198,6 @@ public class MetadataService : IMetadataService
|
||||
/// <remarks>This can be heavy on memory first run</remarks>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
|
@ -17,8 +17,10 @@ namespace API.Services.Tasks.Metadata;
|
||||
|
||||
public interface IWordCountAnalyzerService
|
||||
{
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
Task ScanLibrary(int libraryId, bool forceUpdate = false);
|
||||
Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
||||
Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -40,8 +42,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
||||
_cacheHelper = cacheHelper;
|
||||
}
|
||||
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
|
||||
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
@ -113,7 +114,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
||||
|
||||
}
|
||||
|
||||
public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false)
|
||||
public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId);
|
||||
@ -126,7 +127,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, series.Name));
|
||||
|
||||
await ProcessSeries(series);
|
||||
await ProcessSeries(series, forceUpdate);
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
|
@ -164,27 +164,44 @@ namespace API.Services.Tasks.Scanner
|
||||
info.Series = MergeName(info);
|
||||
|
||||
var normalizedSeries = Parser.Parser.Normalize(info.Series);
|
||||
var normalizedSortSeries = Parser.Parser.Normalize(info.SeriesSort);
|
||||
var normalizedLocalizedSeries = Parser.Parser.Normalize(info.LocalizedSeries);
|
||||
var existingKey = _scannedSeries.Keys.FirstOrDefault(ps =>
|
||||
ps.Format == info.Format && (ps.NormalizedName == normalizedSeries
|
||||
|| ps.NormalizedName == normalizedLocalizedSeries));
|
||||
existingKey ??= new ParsedSeries()
|
||||
{
|
||||
Format = info.Format,
|
||||
Name = info.Series,
|
||||
NormalizedName = normalizedSeries
|
||||
};
|
||||
|
||||
_scannedSeries.AddOrUpdate(existingKey, new List<ParserInfo>() {info}, (_, oldValue) =>
|
||||
try
|
||||
{
|
||||
oldValue ??= new List<ParserInfo>();
|
||||
if (!oldValue.Contains(info))
|
||||
var existingKey = _scannedSeries.Keys.SingleOrDefault(ps =>
|
||||
ps.Format == info.Format && (ps.NormalizedName.Equals(normalizedSeries)
|
||||
|| ps.NormalizedName.Equals(normalizedLocalizedSeries)
|
||||
|| ps.NormalizedName.Equals(normalizedSortSeries)));
|
||||
existingKey ??= new ParsedSeries()
|
||||
{
|
||||
oldValue.Add(info);
|
||||
}
|
||||
Format = info.Format,
|
||||
Name = info.Series,
|
||||
NormalizedName = normalizedSeries
|
||||
};
|
||||
|
||||
return oldValue;
|
||||
});
|
||||
_scannedSeries.AddOrUpdate(existingKey, new List<ParserInfo>() {info}, (_, oldValue) =>
|
||||
{
|
||||
oldValue ??= new List<ParserInfo>();
|
||||
if (!oldValue.Contains(info))
|
||||
{
|
||||
oldValue.Add(info);
|
||||
}
|
||||
|
||||
return oldValue;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex, "{SeriesName} matches against multiple series in the parsed series. This indicates a critical kavita issue. Key will be skipped", info.Series);
|
||||
foreach (var seriesKey in _scannedSeries.Keys.Where(ps =>
|
||||
ps.Format == info.Format && (ps.NormalizedName.Equals(normalizedSeries)
|
||||
|| ps.NormalizedName.Equals(normalizedLocalizedSeries)
|
||||
|| ps.NormalizedName.Equals(normalizedSortSeries))))
|
||||
{
|
||||
_logger.LogCritical("Matches: {SeriesName} matches on {SeriesKey}", info.Series, seriesKey.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -198,14 +215,32 @@ namespace API.Services.Tasks.Scanner
|
||||
var normalizedSeries = Parser.Parser.Normalize(info.Series);
|
||||
var normalizedLocalSeries = Parser.Parser.Normalize(info.LocalizedSeries);
|
||||
// We use FirstOrDefault because this was introduced late in development and users might have 2 series with both names
|
||||
var existingName =
|
||||
_scannedSeries.FirstOrDefault(p =>
|
||||
(Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries ||
|
||||
Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) && p.Key.Format == info.Format)
|
||||
.Key;
|
||||
if (existingName != null && !string.IsNullOrEmpty(existingName.Name))
|
||||
try
|
||||
{
|
||||
return existingName.Name;
|
||||
var existingName =
|
||||
_scannedSeries.SingleOrDefault(p =>
|
||||
(Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries ||
|
||||
Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) &&
|
||||
p.Key.Format == info.Format)
|
||||
.Key;
|
||||
|
||||
if (existingName != null && !string.IsNullOrEmpty(existingName.Name))
|
||||
{
|
||||
return existingName.Name;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex, "Multiple series detected for {SeriesName} ({File})! This is critical to fix! There should only be 1", info.Series, info.FullFilePath);
|
||||
var values = _scannedSeries.Where(p =>
|
||||
(Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries ||
|
||||
Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) &&
|
||||
p.Key.Format == info.Format);
|
||||
foreach (var pair in values)
|
||||
{
|
||||
_logger.LogCritical("Duplicate Series in DB matches with {SeriesName}: {DuplicateName}", info.Series, pair.Key.Name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return info.Series;
|
||||
|
@ -28,8 +28,14 @@ public interface IScannerService
|
||||
/// cover images if forceUpdate is true.
|
||||
/// </summary>
|
||||
/// <param name="libraryId">Library to scan against</param>
|
||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
Task ScanLibrary(int libraryId);
|
||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
Task ScanLibraries();
|
||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
Task ScanSeries(int libraryId, int seriesId, CancellationToken token);
|
||||
}
|
||||
|
||||
@ -63,8 +69,6 @@ public class ScannerService : IScannerService
|
||||
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||
}
|
||||
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanSeries(int libraryId, int seriesId, CancellationToken token)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
@ -247,8 +251,6 @@ public class ScannerService : IScannerService
|
||||
}
|
||||
|
||||
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60 * 4)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanLibraries()
|
||||
{
|
||||
_logger.LogInformation("Starting Scan of All Libraries");
|
||||
@ -267,8 +269,7 @@ public class ScannerService : IScannerService
|
||||
/// ie) all entities will be rechecked for new cover images and comicInfo.xml changes
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
|
||||
public async Task ScanLibrary(int libraryId)
|
||||
{
|
||||
Library library;
|
||||
|
@ -5,7 +5,7 @@
|
||||
"TokenKey": "super secret unguessable key",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Default": "Debug",
|
||||
"Microsoft": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Error",
|
||||
"Hangfire": "Information",
|
||||
|
@ -133,6 +133,14 @@ export class ReaderService {
|
||||
return this.httpClient.get<HourEstimateRange>(this.baseUrl + 'reader/time-left?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
getTimeToRead(seriesId: number) {
|
||||
return this.httpClient.get<HourEstimateRange>(this.baseUrl + 'reader/read-time?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
getManualTimeToRead(wordCount: number, pageCount: number, isEpub: boolean) {
|
||||
return this.httpClient.get<HourEstimateRange>(this.baseUrl + 'reader/manual-read-time?wordCount=' + wordCount + '&pageCount=' + pageCount + '&isEpub=' + isEpub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures current body color and forces background color to be black. Call @see resetOverrideStyles() on destroy of component to revert changes
|
||||
*/
|
||||
|
@ -52,7 +52,7 @@
|
||||
<ng-container *ngIf="chapter.pages">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Pages">
|
||||
{{chapter.pages}} Pages
|
||||
{{chapter.pages | number:''}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
@ -70,7 +70,7 @@
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && chapterMetadata !== undefined && chapterMetadata.wordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
{{minHoursToRead}}{{maxHoursToRead !== minHoursToRead ? ('-' + maxHoursToRead) : ''}} Hour{{minHoursToRead > 1 ? 's' : ''}}
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
@ -229,7 +229,7 @@
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Pages: {{file.pages}}
|
||||
Pages: {{file.pages | number:''}}
|
||||
</div>
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
Added: {{(data.created | date: 'short') || '-'}}
|
||||
|
@ -6,6 +6,7 @@ import { Observable, of, take } from 'rxjs';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { ChapterMetadata } from 'src/app/_models/chapter-metadata';
|
||||
import { HourEstimateRange } from 'src/app/_models/hour-estimate-range';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
@ -77,10 +78,12 @@ export class CardDetailDrawerComponent implements OnInit {
|
||||
ageRating!: string;
|
||||
|
||||
summary$: Observable<string> = of('');
|
||||
readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1, hasProgress: false};
|
||||
minHoursToRead: number = 1;
|
||||
maxHoursToRead: number = 1;
|
||||
|
||||
|
||||
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
}
|
||||
@ -120,13 +123,13 @@ export class CardDetailDrawerComponent implements OnInit {
|
||||
|
||||
this.metadataService.getAgeRating(this.chapterMetadata.ageRating).subscribe(ageRating => this.ageRating = ageRating);
|
||||
|
||||
if (this.chapter.files[0].format === MangaFormat.EPUB && this.chapterMetadata.wordCount > 0) {
|
||||
this.minHoursToRead = parseInt(Math.round(this.chapterMetadata.wordCount / MAX_WORDS_PER_HOUR) + '', 10) || 1;
|
||||
this.maxHoursToRead = parseInt(Math.round(this.chapterMetadata.wordCount / MIN_WORDS_PER_HOUR) + '', 10) || 1;
|
||||
} else if (this.chapter.files[0].format !== MangaFormat.EPUB) {
|
||||
this.minHoursToRead = parseInt(Math.round((this.chapter.pages / MIN_PAGES_PER_MINUTE) / 60) + '', 10) || 1;
|
||||
this.maxHoursToRead = parseInt(Math.round((this.chapter.pages / MAX_PAGES_PER_MINUTE) / 60) + '', 10) || 1;
|
||||
let totalPages = this.chapter.pages;
|
||||
if (!this.isChapter) {
|
||||
// Need to account for multiple chapters if this is a volume
|
||||
totalPages = this.utilityService.asVolume(this.data).chapters.map(c => c.pages).reduce((sum, d) => sum + d);
|
||||
}
|
||||
|
||||
this.readerService.getManualTimeToRead(this.chapterMetadata.wordCount, totalPages, this.chapter.files[0].format === MangaFormat.EPUB).subscribe((time) => this.readingTime = time);
|
||||
});
|
||||
|
||||
|
||||
|
@ -67,29 +67,28 @@
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-template #showPages>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{series.pages}} Pages
|
||||
{{series.pages | number:''}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
|
||||
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
{{minHoursToRead}}{{maxHoursToRead !== minHoursToRead ? ('-' + maxHoursToRead) : ''}} Hour{{minHoursToRead > 1 ? 's' : ''}}
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="readingTimeLeft.hasProgress && readingTimeLeft.minHours !== 1 && readingTimeLeft.maxHours !== 1 && readingTimeLeft.avgHours !== 0">
|
||||
<ng-container *ngIf="readingTimeLeft.hasProgress && readingTimeLeft.avgHours !== 0 ">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-solid fa-clock">
|
||||
|
@ -29,8 +29,7 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
||||
isCollapsed: boolean = true;
|
||||
hasExtendedProperites: boolean = false;
|
||||
|
||||
minHoursToRead: number = 1;
|
||||
maxHoursToRead: number = 1;
|
||||
readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1, hasProgress: false};
|
||||
readingTimeLeft: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1, hasProgress: false};
|
||||
|
||||
/**
|
||||
@ -71,14 +70,7 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
||||
|
||||
if (this.series !== null) {
|
||||
this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => this.readingTimeLeft = timeLeft);
|
||||
|
||||
if (this.series.format === MangaFormat.EPUB && this.series.wordCount > 0) {
|
||||
this.minHoursToRead = parseInt(Math.round(this.series.wordCount / MAX_WORDS_PER_HOUR) + '', 10) || 1;
|
||||
this.maxHoursToRead = parseInt(Math.round(this.series.wordCount / MIN_WORDS_PER_HOUR) + '', 10) || 1;
|
||||
} else if (this.series.format !== MangaFormat.EPUB) {
|
||||
this.minHoursToRead = parseInt(Math.round((this.series.pages / MIN_PAGES_PER_MINUTE) / 60) + '', 10) || 1;
|
||||
this.maxHoursToRead = parseInt(Math.round((this.series.pages / MAX_PAGES_PER_MINUTE) / 60) + '', 10) || 1;
|
||||
}
|
||||
this.readerService.getTimeToRead(this.series.id).subscribe((time) => this.readingTime = time);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user