First PR of the new year (#1717)
* Fixed a bug on bookmark mode not finding correct image for prefetcher. * Fixed up the edit series relationship modal on tablet viewports. * On double page mode, only bookmark 1 page if only 1 pages is renderered on screen. * Added percentage read of a given library and average hours read per week to user stats. * Fixed a bug in the reader with paging in bookmark mode * Added a "This Week" option to top readers history * Added date ranges for reading time. Added dates that don't have anything, but might remove. * On phone, when applying a metadata filter, when clicking apply, collapse the filter automatically. * Disable jump bar and the resuming from last spot when a custom sort is applied. * Ensure all Regex.Replace or Matches have timeouts set * Fixed a long standing bug where fit to height on tablets wouldn't center the image * Streamlined url parsing to be more reliable * Reduced an additional db query in chapter info. * Added a missing task to convert covers to webP and added messaging to help the user understand to run it after modifying the setting. * Changed OPDS to be enabled by default for new installs. This should reduce issues with users being confused about it before it's enabled. * When there are multiple files for a chapter, show a count card on the series detail to help user understand duplicates exist. Made the unread badge smaller to avoid collision. * Added Word Count to user stats and wired up average reading per week. * Fixed word count failing on some epubs * Removed some debug code * Don't give more information than is necessary about file paths for page dimensions. * Fixed a bug where pagination area would be too small when the book's content was less that height on default mode. * Updated Default layout mode to Scroll for books. * Added bytes in the UI and at an API layer for CDisplayEx * Don't log health checks to logs at all. * Changed Word Count to Length to match the way pages work * Made reading time more clear when min hours is 0 * Apply more aggressive coalescing when remapping bad metadata keys for epubs. * Changed the amount of padding between icon and text for side nav item. * Fixed a NPE on book reader (harmless) * Fixed an ordering issue where Volume 1 was a single file but also tagged as Chapter 1 and Volume 2 was Chapter 0. Thus Volume 2 was being selected for continue point when Volume 1 should have been. * When clicking on an activity stream header from dashboard, show the title on the resulting page. * Removed a property that can't be animated * Fixed a typeahead typescript issue * Added Size into Series Info and Added some tooltip and spacing changes to better explain some fields. * Added size for volume drawers and cleaned up some date edge case handling * Fixed an annoying bug where when on mobile opening a view with a metadata filter, Kavita would open the filter automatically.
This commit is contained in:
parent
8eb5b466ef
commit
a545f96a05
52 changed files with 410 additions and 187 deletions
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -8,7 +7,6 @@ using API.DTOs.Reader;
|
|||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using Kavita.Common;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using VersOne.Epub;
|
||||
|
@ -97,7 +95,7 @@ public class BookController : BaseApiController
|
|||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
|
||||
var key = BookService.CleanContentKeys(file);
|
||||
var key = BookService.CoalesceKeyForAnyFile(book, file);
|
||||
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
|
||||
|
||||
var bookFile = book.Content.AllFiles[key];
|
||||
|
|
|
@ -187,7 +187,7 @@ public class ReaderController : BaseApiController
|
|||
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
if (dto == null) return BadRequest("Please perform a scan on this series or library and try again");
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
var mangaFile = chapter.Files.First();
|
||||
|
||||
var info = new ChapterInfoDto()
|
||||
{
|
||||
|
|
|
@ -143,6 +143,19 @@ public class ServerController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the scheduling of the convert covers job. Only one job will run at a time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("convert-covers")]
|
||||
public ActionResult ScheduleConvertCovers()
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true)) return Ok();
|
||||
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllCoverToWebP());
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
public ActionResult GetLogs()
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@ public class MangaFileDto
|
|||
public int Id { get; init; }
|
||||
public string FilePath { get; init; }
|
||||
public int Pages { get; init; }
|
||||
public long Bytes { get; init; }
|
||||
public MangaFormat Format { get; init; }
|
||||
public DateTime Created { get; init; }
|
||||
|
||||
|
|
|
@ -10,6 +10,10 @@ public class UserReadStatistics
|
|||
/// </summary>
|
||||
public long TotalPagesRead { get; set; }
|
||||
/// <summary>
|
||||
/// Total number of words read
|
||||
/// </summary>
|
||||
public long TotalWordsRead { get; set; }
|
||||
/// <summary>
|
||||
/// Total time spent reading based on estimates
|
||||
/// </summary>
|
||||
public long TimeSpentReading { get; set; }
|
||||
|
|
|
@ -18,6 +18,7 @@ public enum ChapterIncludes
|
|||
{
|
||||
None = 1,
|
||||
Volumes = 2,
|
||||
Files = 4
|
||||
}
|
||||
|
||||
public interface IChapterRepository
|
||||
|
@ -26,7 +27,7 @@ public interface IChapterRepository
|
|||
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds, ChapterIncludes includes = ChapterIncludes.None);
|
||||
Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId);
|
||||
Task<int> GetChapterTotalPagesAsync(int chapterId);
|
||||
Task<Chapter> GetChapterAsync(int chapterId);
|
||||
Task<Chapter> GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
|
||||
Task<ChapterDto> GetChapterDtoAsync(int chapterId);
|
||||
Task<ChapterMetadataDto> GetChapterMetadataDtoAsync(int chapterId);
|
||||
Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId);
|
||||
|
@ -34,6 +35,7 @@ public interface IChapterRepository
|
|||
Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
|
||||
Task<string> GetChapterCoverImageAsync(int chapterId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<IList<Chapter>> GetAllChaptersWithNonWebPCovers();
|
||||
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
|
||||
}
|
||||
public class ChapterRepository : IChapterRepository
|
||||
|
@ -162,12 +164,17 @@ public class ChapterRepository : IChapterRepository
|
|||
/// Returns a Chapter for an Id. Includes linked <see cref="MangaFile"/>s.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="includes"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Chapter> GetChapterAsync(int chapterId)
|
||||
public async Task<Chapter> GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Include(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
var query = _context.Chapter
|
||||
.AsSplitQuery();
|
||||
|
||||
if (includes.HasFlag(ChapterIncludes.Files)) query = query.Include(c => c.Files);
|
||||
if (includes.HasFlag(ChapterIncludes.Volumes)) query = query.Include(c => c.Volume);
|
||||
|
||||
return await query
|
||||
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
||||
}
|
||||
|
||||
|
@ -207,6 +214,13 @@ public class ChapterRepository : IChapterRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Chapter>> GetAllChaptersWithNonWebPCovers()
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(".webp"))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover images for locked chapters
|
||||
/// </summary>
|
||||
|
|
|
@ -90,7 +90,7 @@ public static class Seed
|
|||
Key = ServerSettingKey.Port, Value = "5000"
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||
new() {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||
new() {Key = ServerSettingKey.EnableOpds, Value = "true"},
|
||||
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
|
||||
new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
|
||||
|
|
|
@ -15,5 +15,6 @@ public static class LogEnricher
|
|||
{
|
||||
diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress?.ToString());
|
||||
diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].FirstOrDefault());
|
||||
diagnosticContext.Set("Path", httpContext.Request.Path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
|
@ -57,7 +60,18 @@ public static class LogLevelOptions
|
|||
.WriteTo.File(LogFile,
|
||||
shared: true,
|
||||
rollingInterval: RollingInterval.Day,
|
||||
outputTemplate: outputTemplate);
|
||||
outputTemplate: outputTemplate)
|
||||
.Filter.ByIncludingOnly(ShouldIncludeLogStatement);
|
||||
}
|
||||
|
||||
private static bool ShouldIncludeLogStatement(LogEvent e)
|
||||
{
|
||||
if (e.Properties.ContainsKey("SourceContext") &&
|
||||
e.Properties["SourceContext"].ToString().Replace("\"", string.Empty) == "Serilog.AspNetCore.RequestLoggingMiddleware")
|
||||
{
|
||||
if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/api/health") return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SwitchLogLevel(string level)
|
||||
|
|
|
@ -33,17 +33,6 @@ public interface IBookService
|
|||
{
|
||||
int GetNumberOfPages(string filePath);
|
||||
string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
Task<Dictionary<string, int>> CreateKeyToPageMappingAsync(EpubBookRef book);
|
||||
|
||||
/// <summary>
|
||||
/// Scopes styles to .reading-section and replaces img src to the passed apiBase
|
||||
/// </summary>
|
||||
/// <param name="stylesheetHtml"></param>
|
||||
/// <param name="apiBase"></param>
|
||||
/// <param name="filename">If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath.</param>
|
||||
/// <param name="book">Book Reference, needed for if you expect Import statements</param>
|
||||
/// <returns></returns>
|
||||
Task<string> ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book);
|
||||
ComicInfo GetComicInfo(string filePath);
|
||||
ParserInfo ParseInfo(string filePath);
|
||||
/// <summary>
|
||||
|
@ -53,11 +42,9 @@ public interface IBookService
|
|||
/// <param name="fileFilePath"></param>
|
||||
/// <param name="targetDirectory">Where the files will be extracted to. If doesn't exist, will be created.</param>
|
||||
void ExtractPdfImages(string fileFilePath, string targetDirectory);
|
||||
|
||||
Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page);
|
||||
Task<ICollection<BookChapterItem>> GenerateTableOfContents(Chapter chapter);
|
||||
|
||||
Task<string> GetBookPage(int page, int chapterId, string cachedEpubPath, string baseUrl);
|
||||
Task<Dictionary<string, int>> CreateKeyToPageMappingAsync(EpubBookRef book);
|
||||
}
|
||||
|
||||
public class BookService : IBookService
|
||||
|
@ -163,6 +150,14 @@ public class BookService : IBookService
|
|||
anchor.Attributes.Add("href", "javascript:void(0)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scopes styles to .reading-section and replaces img src to the passed apiBase
|
||||
/// </summary>
|
||||
/// <param name="stylesheetHtml"></param>
|
||||
/// <param name="apiBase"></param>
|
||||
/// <param name="filename">If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath.</param>
|
||||
/// <param name="book">Book Reference, needed for if you expect Import statements</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book)
|
||||
{
|
||||
// @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be Scoped
|
||||
|
@ -717,6 +712,13 @@ public class BookService : IBookService
|
|||
return PrepareFinalHtml(doc, body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find the correct key by applying cleaning and remapping if the epub has bad data. Only works for HTML files.
|
||||
/// </summary>
|
||||
/// <param name="book"></param>
|
||||
/// <param name="mappings"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
private static string CoalesceKey(EpubBookRef book, IDictionary<string, int> mappings, string key)
|
||||
{
|
||||
if (mappings.ContainsKey(CleanContentKeys(key))) return key;
|
||||
|
@ -731,6 +733,23 @@ public class BookService : IBookService
|
|||
return key;
|
||||
}
|
||||
|
||||
public static string CoalesceKeyForAnyFile(EpubBookRef book, string key)
|
||||
{
|
||||
if (book.Content.AllFiles.ContainsKey(key)) return key;
|
||||
|
||||
var cleanedKey = CleanContentKeys(key);
|
||||
if (book.Content.AllFiles.ContainsKey(cleanedKey)) return cleanedKey;
|
||||
|
||||
// Fallback to searching for key (bad epub metadata)
|
||||
var correctedKey = book.Content.AllFiles.Keys.SingleOrDefault(s => s.EndsWith(key));
|
||||
if (!string.IsNullOrEmpty(correctedKey))
|
||||
{
|
||||
key = correctedKey;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
|
||||
/// this is used to rewrite anchors in the book text so that we always load properly in our reader.
|
||||
|
@ -844,7 +863,7 @@ public class BookService : IBookService
|
|||
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return content;
|
||||
|
||||
// In more cases than not, due to this being XML not HTML, we need to escape the script tags.
|
||||
content = BookService.EscapeTags(content);
|
||||
content = EscapeTags(content);
|
||||
|
||||
doc.LoadHtml(content);
|
||||
var body = doc.DocumentNode.SelectSingleNode("//body");
|
||||
|
|
|
@ -22,6 +22,7 @@ public interface IBookmarkService
|
|||
Task<IEnumerable<string>> GetBookmarkFilesById(IEnumerable<int> bookmarkIds);
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
Task ConvertAllBookmarkToWebP();
|
||||
Task ConvertAllCoverToWebP();
|
||||
|
||||
}
|
||||
|
||||
|
@ -183,7 +184,9 @@ public class BookmarkService : IBookmarkService
|
|||
var count = 1F;
|
||||
foreach (var bookmark in bookmarks)
|
||||
{
|
||||
await SaveBookmarkAsWebP(bookmarkDirectory, bookmark);
|
||||
bookmark.FileName = await SaveAsWebP(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId));
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Started));
|
||||
|
@ -196,10 +199,40 @@ public class BookmarkService : IBookmarkService
|
|||
_logger.LogInformation("[BookmarkService] Converted bookmarks to WebP");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a long-running job that will convert all covers into WebP. Do not invoke anyway except via Hangfire.
|
||||
/// </summary>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
public async Task ConvertAllCoverToWebP()
|
||||
{
|
||||
var coverDirectory = _directoryService.CoverImageDirectory;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started));
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetAllChaptersWithNonWebPCovers();
|
||||
|
||||
var count = 1F;
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
var newFile = await SaveAsWebP(coverDirectory, chapter.CoverImage, coverDirectory);
|
||||
chapter.CoverImage = newFile;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / chapters.Count, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
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)
|
||||
private async Task ConvertBookmarkToWebP(int bookmarkId)
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
|
@ -212,46 +245,52 @@ public class BookmarkService : IBookmarkService
|
|||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId);
|
||||
if (bookmark == null) return;
|
||||
|
||||
await SaveBookmarkAsWebP(bookmarkDirectory, bookmark);
|
||||
bookmark.FileName = await SaveAsWebP(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId));
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts bookmark file, deletes original, marks bookmark as dirty. Does not commit.
|
||||
/// Converts an image file, deletes original and returns the new path back
|
||||
/// </summary>
|
||||
/// <param name="bookmarkDirectory"></param>
|
||||
/// <param name="bookmark"></param>
|
||||
private async Task SaveBookmarkAsWebP(string bookmarkDirectory, AppUserBookmark bookmark)
|
||||
/// <param name="imageDirectory">Full Path to where files are stored</param>
|
||||
/// <param name="filename">The file to convert</param>
|
||||
/// <param name="targetFolder">Full path to where files should be stored or any stem</param>
|
||||
/// <returns></returns>
|
||||
private async Task<string> SaveAsWebP(string imageDirectory, string filename, string targetFolder)
|
||||
{
|
||||
var fullSourcePath = _directoryService.FileSystem.Path.Join(bookmarkDirectory, bookmark.FileName);
|
||||
var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(bookmark.FileName).Name, string.Empty);
|
||||
var targetFolderStem = BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId);
|
||||
var fullSourcePath = _directoryService.FileSystem.Path.Join(imageDirectory, filename);
|
||||
var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty);
|
||||
|
||||
_logger.LogDebug("Converting {Source} bookmark into WebP at {Target}", fullSourcePath, fullTargetDirectory);
|
||||
var newFilename = string.Empty;
|
||||
_logger.LogDebug("Converting {Source} image into WebP at {Target}", fullSourcePath, fullTargetDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
// Convert target file to webp then delete original target file and update bookmark
|
||||
|
||||
var originalFile = bookmark.FileName;
|
||||
var originalFile = filename;
|
||||
try
|
||||
{
|
||||
var targetFile = await _imageService.ConvertToWebP(fullSourcePath, fullTargetDirectory);
|
||||
var targetName = new FileInfo(targetFile).Name;
|
||||
bookmark.FileName = Path.Join(targetFolderStem, targetName);
|
||||
newFilename = Path.Join(targetFolder, targetName);
|
||||
_directoryService.DeleteFiles(new[] {fullSourcePath});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not convert file {FilePath}", bookmark.FileName);
|
||||
bookmark.FileName = originalFile;
|
||||
_logger.LogError(ex, "Could not convert image {FilePath}", filename);
|
||||
newFilename = originalFile;
|
||||
}
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not convert bookmark to WebP");
|
||||
_logger.LogError(ex, "Could not convert image to WebP");
|
||||
}
|
||||
|
||||
return newFilename;
|
||||
}
|
||||
|
||||
private static string BookmarkStem(int userId, int seriesId, int chapterId)
|
||||
|
|
|
@ -82,7 +82,7 @@ public class CacheService : ICacheService
|
|||
PageNumber = i,
|
||||
Height = image.Height,
|
||||
Width = image.Width,
|
||||
FileName = file
|
||||
FileName = file.Replace(path, string.Empty)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.IO;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
using SixLabors.ImageSharp;
|
||||
using Image = NetVips.Image;
|
||||
|
||||
namespace API.Services;
|
||||
|
|
|
@ -479,11 +479,14 @@ public class ReaderService : IReaderService
|
|||
var volumeChapters = volumes
|
||||
.Where(v => v.Number != 0)
|
||||
.SelectMany(v => v.Chapters)
|
||||
.OrderBy(c => float.Parse(c.Number))
|
||||
//.OrderBy(c => float.Parse(c.Number))
|
||||
.ToList();
|
||||
|
||||
// NOTE: If volume 1 has chapter 1 and volume 2 is just chapter 0 due to being a full volume file, then this fails
|
||||
// If there are any volumes that have progress, return those. If not, move on.
|
||||
var currentlyReadingChapter = volumeChapters.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages);
|
||||
var currentlyReadingChapter = volumeChapters
|
||||
.OrderBy(c => double.Parse(c.Range), _chapterSortComparer)
|
||||
.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages);
|
||||
if (currentlyReadingChapter != null) return currentlyReadingChapter;
|
||||
|
||||
// Order with volume 0 last so we prefer the natural order
|
||||
|
|
|
@ -70,6 +70,10 @@ public class StatisticService : IStatisticService
|
|||
.Where(c => chapterIds.Contains(c.Id))
|
||||
.SumAsync(c => c.AvgHoursToRead);
|
||||
|
||||
var totalWordsRead = await _context.Chapter
|
||||
.Where(c => chapterIds.Contains(c.Id))
|
||||
.SumAsync(c => c.WordCount);
|
||||
|
||||
var chaptersRead = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId)
|
||||
.Where(p => libraryIds.Contains(p.LibraryId))
|
||||
|
@ -90,8 +94,7 @@ public class StatisticService : IStatisticService
|
|||
.AsEnumerable()
|
||||
.GroupBy(g => g.series.LibraryId)
|
||||
.ToDictionary(g => g.Key, g => g.Sum(c => c.chapter.Pages));
|
||||
//
|
||||
//
|
||||
|
||||
var totalProgressByLibrary = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId)
|
||||
.Where(p => p.LibraryId > 0)
|
||||
|
@ -108,11 +111,12 @@ public class StatisticService : IStatisticService
|
|||
.Where(p => p.AppUserId == userId)
|
||||
.Join(_context.Chapter, p => p.ChapterId, c => c.Id,
|
||||
(p, c) => (p.PagesRead / (float) c.Pages) * c.AvgHoursToRead)
|
||||
.Average() / 7;
|
||||
.Average() / 7.0;
|
||||
|
||||
return new UserReadStatistics()
|
||||
{
|
||||
TotalPagesRead = totalPagesRead,
|
||||
TotalWordsRead = totalWordsRead,
|
||||
TimeSpentReading = timeSpentReading,
|
||||
ChaptersRead = chaptersRead,
|
||||
LastActive = lastActive,
|
||||
|
@ -314,7 +318,7 @@ public class StatisticService : IStatisticService
|
|||
.Select(u => new ReadHistoryEvent
|
||||
{
|
||||
UserId = u.AppUserId,
|
||||
UserName = _context.AppUser.Single(u => u.Id == userId).UserName,
|
||||
UserName = _context.AppUser.Single(u2 => u2.Id == userId).UserName,
|
||||
SeriesName = _context.Series.Single(s => s.Id == u.SeriesId).Name,
|
||||
SeriesId = u.SeriesId,
|
||||
LibraryId = u.LibraryId,
|
||||
|
|
|
@ -243,8 +243,9 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(await bookFile.ReadContentAsTextAsync());
|
||||
|
||||
return doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]")
|
||||
.DefaultIfEmpty()
|
||||
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
|
||||
if (textNodes == null) return 0;
|
||||
return textNodes
|
||||
.Select(node => node.InnerText.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(s => char.IsLetter(s[0])))
|
||||
.Sum(words => words.Count());
|
||||
|
|
|
@ -106,6 +106,10 @@ public static class MessageFactory
|
|||
/// </summary>
|
||||
private const string ConvertBookmarksProgress = "ConvertBookmarksProgress";
|
||||
/// <summary>
|
||||
/// When bulk covers are being converted
|
||||
/// </summary>
|
||||
private const string ConvertCoversProgress = "ConvertBookmarksProgress";
|
||||
/// <summary>
|
||||
/// When files are being scanned to calculate word count
|
||||
/// </summary>
|
||||
private const string WordCountAnalyzerProgress = "WordCountAnalyzerProgress";
|
||||
|
@ -495,4 +499,21 @@ public static class MessageFactory
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static SignalRMessage ConvertCoverProgressEvent(float progress, string eventType)
|
||||
{
|
||||
return new SignalRMessage()
|
||||
{
|
||||
Name = ConvertCoversProgress,
|
||||
Title = "Converting Covers to WebP",
|
||||
SubTitle = string.Empty,
|
||||
EventType = eventType,
|
||||
Progress = ProgressType.Determinate,
|
||||
Body = new
|
||||
{
|
||||
Progress = progress,
|
||||
EventTime = DateTime.Now
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue