Next Estimated Chapter (#2342)
This commit is contained in:
parent
ca5afe94d3
commit
de9b09c71f
32 changed files with 433 additions and 73 deletions
|
@ -1026,16 +1026,26 @@ public class BookService : IBookService
|
|||
|
||||
if (chaptersList.Count != 0) return chaptersList;
|
||||
// Generate from TOC from links (any point past this, Kavita is generating as a TOC doesn't exist)
|
||||
var tocPage = book.Content.Html.Local.Select(s => s.Key).FirstOrDefault(k => k.Equals("TOC.XHTML", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
var tocPage = book.Content.Html.Local.Select(s => s.Key)
|
||||
.FirstOrDefault(k => k.Equals("TOC.XHTML", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
k.Equals("NAVIGATION.XHTML", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (string.IsNullOrEmpty(tocPage)) return chaptersList;
|
||||
|
||||
|
||||
// Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content
|
||||
if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file)) return chaptersList;
|
||||
var content = await file.ReadContentAsync();
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(content);
|
||||
|
||||
// TODO: We may want to check if there is a toc.ncs file to better handle nested toc
|
||||
// We could do a fallback first with ol/lis
|
||||
//var sections = doc.DocumentNode.SelectNodes("//ol");
|
||||
//if (sections == null)
|
||||
|
||||
|
||||
|
||||
var anchors = doc.DocumentNode.SelectNodes("//a");
|
||||
if (anchors == null) return chaptersList;
|
||||
|
||||
|
|
|
@ -206,8 +206,7 @@ public class ScrobblingService : IScrobblingService
|
|||
ScrobbleEventType.Review);
|
||||
if (existingEvt is {IsProcessed: false})
|
||||
{
|
||||
_logger.LogDebug("Overriding scrobble event for {Series} from Review {Tagline}/{Body} -> {UpdatedTagline}{UpdatedBody}",
|
||||
existingEvt.Series.Name, existingEvt.ReviewTitle, existingEvt.ReviewBody, reviewTitle, reviewBody);
|
||||
_logger.LogDebug("Overriding Review scrobble event for {Series}", existingEvt.Series.Name);
|
||||
existingEvt.ReviewBody = reviewBody;
|
||||
existingEvt.ReviewTitle = reviewTitle;
|
||||
_unitOfWork.ScrobbleRepository.Update(existingEvt);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -17,6 +18,7 @@ using API.Helpers.Builders;
|
|||
using API.Services.Plus;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
@ -36,6 +38,7 @@ public interface ISeriesService
|
|||
Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle,
|
||||
bool withHash);
|
||||
Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false);
|
||||
Task<NextExpectedChapterDto> GetEstimatedChapterCreationDate(int seriesId, int userId);
|
||||
}
|
||||
|
||||
public class SeriesService : ISeriesService
|
||||
|
@ -399,6 +402,7 @@ public class SeriesService : ISeriesService
|
|||
public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var libraryIds = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId);
|
||||
if (!libraryIds.Contains(series.LibraryId))
|
||||
throw new UnauthorizedAccessException("user-no-access-library-from-series");
|
||||
|
@ -488,7 +492,7 @@ public class SeriesService : ISeriesService
|
|||
Volumes = processedVolumes,
|
||||
StorylineChapters = storylineChapters,
|
||||
TotalCount = chapters.Count,
|
||||
UnreadCount = chapters.Count(c => c.Pages > 0 && c.PagesRead < c.Pages)
|
||||
UnreadCount = chapters.Count(c => c.Pages > 0 && c.PagesRead < c.Pages),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -642,4 +646,88 @@ public class SeriesService : ISeriesService
|
|||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<NextExpectedChapterDto> GetEstimatedChapterCreationDate(int seriesId, int userId)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var libraryIds = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId);
|
||||
if (!libraryIds.Contains(series.LibraryId)) //// TODO: Rewrite this to use a new method which checks permissions all in the DB to be streamlined and less memory
|
||||
throw new UnauthorizedAccessException("user-no-access-library-from-series");
|
||||
if (series?.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book)
|
||||
{
|
||||
return new NextExpectedChapterDto()
|
||||
{
|
||||
ExpectedDate = null,
|
||||
ChapterNumber = 0,
|
||||
VolumeNumber = 0
|
||||
};
|
||||
}
|
||||
|
||||
var chapters = _unitOfWork.ChapterRepository.GetChaptersForSeries(seriesId)
|
||||
.Where(c => !c.IsSpecial)
|
||||
.OrderBy(c => c.CreatedUtc)
|
||||
.ToList();
|
||||
|
||||
// Calculate the time differences between consecutive chapters
|
||||
var timeDifferences = chapters
|
||||
.Select((chapter, index) => new
|
||||
{
|
||||
ChapterNumber = chapter.Number,
|
||||
VolumeNumber = chapter.Volume.Number,
|
||||
TimeDifference = index == 0 ? TimeSpan.Zero : (chapter.CreatedUtc - chapters.ElementAt(index - 1).CreatedUtc)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Calculate the average time difference between chapters
|
||||
var averageTimeDifference = timeDifferences
|
||||
.Average(td => td.TimeDifference.TotalDays);
|
||||
|
||||
// Calculate the forecast for when the next chapter is expected
|
||||
var nextChapterExpected = chapters.Any()
|
||||
? chapters.Max(c => c.CreatedUtc) + TimeSpan.FromDays(averageTimeDifference)
|
||||
: (DateTime?) null;
|
||||
|
||||
if (nextChapterExpected != null && nextChapterExpected < DateTime.UtcNow)
|
||||
{
|
||||
nextChapterExpected = DateTime.UtcNow + TimeSpan.FromDays(averageTimeDifference);
|
||||
}
|
||||
|
||||
var lastChapter = timeDifferences.Last();
|
||||
float.TryParse(lastChapter.ChapterNumber, NumberStyles.Number, CultureInfo.InvariantCulture,
|
||||
out var lastChapterNumber);
|
||||
|
||||
var result = new NextExpectedChapterDto()
|
||||
{
|
||||
ChapterNumber = 0,
|
||||
VolumeNumber = 0,
|
||||
ExpectedDate = nextChapterExpected,
|
||||
Title = string.Empty
|
||||
};
|
||||
|
||||
if (lastChapterNumber > 0)
|
||||
{
|
||||
result.ChapterNumber = lastChapterNumber + 1;
|
||||
result.VolumeNumber = lastChapter.VolumeNumber;
|
||||
result.Title = series.Library.Type switch
|
||||
{
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num",
|
||||
new object[] {result.ChapterNumber}),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num",
|
||||
new object[] {"#", result.ChapterNumber}),
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num",
|
||||
new object[] {result.ChapterNumber}),
|
||||
_ => "Chapter " + result.ChapterNumber
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
result.VolumeNumber = lastChapter.VolumeNumber + 1;
|
||||
result.Title = await _localizationService.Translate(userId, "vol-num",
|
||||
new object[] {result.VolumeNumber});
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -288,14 +288,12 @@ public class ProcessSeries : IProcessSeries
|
|||
series.Metadata.TotalCount = chapters.Max(chapter => chapter.TotalCount);
|
||||
// The actual number of count's defined across all chapter's metadata
|
||||
series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count);
|
||||
// To not have to rely completely on ComicInfo, try to parse out if the series is complete by checking parsed filenames as well.
|
||||
if (series.Metadata.MaxCount != series.Metadata.TotalCount)
|
||||
{
|
||||
var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name));
|
||||
var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range));
|
||||
if (maxVolume == series.Metadata.TotalCount) series.Metadata.MaxCount = maxVolume;
|
||||
else if (maxChapter == series.Metadata.TotalCount) series.Metadata.MaxCount = maxChapter;
|
||||
}
|
||||
|
||||
var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name));
|
||||
var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range));
|
||||
var maxActual = Math.Max(maxVolume, maxChapter);
|
||||
|
||||
series.Metadata.MaxCount = maxActual;
|
||||
|
||||
|
||||
if (!series.Metadata.PublicationStatusLocked)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue