Feature/feedback (#185)

* Remove automatic retry for scanLibraries as if something fails, it wont pass magically. Catch exceptions when opening books for parsing and swallow to ignore the file.

* Delete extra attempts

* Switched to using FirstOrDefault for finding existing series. This will help avoid pointless crashes.

* Updated message when duplicate series are found (not sure how this happens)

* Fixed a negation for deleting volumes where files still exist.

* Implemented the ability to automatically scale the manga reader based on screen size.

* Default to automatic scaling

* Fix an issue where malformed epubs wouldn't be readable due to incorrect keys in the OPF. We now check if key is valid and if not, try to correct it. This makes a page load about a second on malformed books.

* Fixed #176. Refactored the recently added query to be restricted to user's access to libraries.

* Fixed a one off bug with In Progress series

* Implemented the ability to refresh metadata of just a single series directly
This commit is contained in:
Joseph Milazzo 2021-04-30 17:23:31 -05:00 committed by GitHub
parent 35a47f5d88
commit 6d74215262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 127 additions and 16 deletions

View File

@ -158,6 +158,19 @@ namespace API.Controllers
foreach (var styleLinks in styleNodes)
{
var key = BookService.CleanContentKeys(styleLinks.Attributes["href"].Value);
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
// In this case, we will do a search for the key that ends with
if (!book.Content.Css.ContainsKey(key))
{
var correctedKey = book.Content.Css.Keys.SingleOrDefault(s => s.EndsWith(key));
if (correctedKey == null)
{
_logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key);
continue;
}
key = correctedKey;
}
var styleContent = await _bookService.ScopeStyles(await book.Content.Css[key].ReadContentAsync(), apiBase);
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
}
@ -183,6 +196,14 @@ namespace API.Controllers
if (image.Attributes["src"] != null)
{
var imageFile = image.Attributes["src"].Value;
if (!book.Content.Images.ContainsKey(imageFile))
{
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
if (correctedKey != null)
{
imageFile = correctedKey;
}
}
image.Attributes.Remove("src");
image.Attributes.Add("src", $"{apiBase}" + imageFile);
}
@ -199,6 +220,14 @@ namespace API.Controllers
if (image.Attributes["xlink:href"] != null)
{
var imageFile = image.Attributes["xlink:href"].Value;
if (!book.Content.Images.ContainsKey(imageFile))
{
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
if (correctedKey != null)
{
imageFile = correctedKey;
}
}
image.Attributes.Remove("xlink:href");
image.Attributes.Add("xlink:href", $"{apiBase}" + imageFile);
}

View File

@ -145,7 +145,8 @@ namespace API.Controllers
[HttpGet("recently-added")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(int libraryId = 0, int limit = 20)
{
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, limit));
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAdded(user.Id, libraryId, limit));
}
[HttpGet("in-progress")]
@ -154,5 +155,13 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, limit));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("refresh-metadata")]
public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto)
{
_taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId);
return Ok();
}
}
}

View File

@ -0,0 +1,8 @@
namespace API.DTOs
{
public class RefreshSeriesDto
{
public int LibraryId { get; set; }
public int SeriesId { get; set; }
}
}

View File

@ -1,4 +1,6 @@
namespace API.DTOs
using System;
namespace API.DTOs
{
public class SeriesDto
{
@ -21,6 +23,8 @@
/// Review from logged in user. Calculated at API-time.
/// </summary>
public string UserReview { get; set; }
public DateTime Created { get; set; }
public int LibraryId { get; set; }
public string LibraryName { get; set; }

View File

@ -80,15 +80,12 @@ namespace API.Data
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams)
{
var sw = Stopwatch.StartNew();
var query = _context.Series
.Where(s => s.LibraryId == libraryId)
.OrderBy(s => s.SortName)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.AsNoTracking();
_logger.LogDebug("Processed GetSeriesDtoForLibraryIdAsync in {ElapsedMilliseconds} milliseconds", sw.ElapsedMilliseconds);
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
@ -295,15 +292,35 @@ namespace API.Data
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
/// <param name="limit">How many series to pick.</param>
/// <returns></returns>
public async Task<IEnumerable<SeriesDto>> GetRecentlyAdded(int libraryId, int limit)
public async Task<IEnumerable<SeriesDto>> GetRecentlyAdded(int userId, int libraryId, int limit)
{
if (libraryId == 0)
{
var userLibraries = _context.Library
.Include(l => l.AppUsers)
.Where(library => library.AppUsers.Any(user => user.Id == userId))
.AsNoTracking()
.Select(library => library.Id)
.ToList();
return await _context.Series
.Where(s => userLibraries.Contains(s.LibraryId))
.AsNoTracking()
.OrderByDescending(s => s.Created)
.Take(limit)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
return await _context.Series
.Where(s => (libraryId <= 0 || s.LibraryId == libraryId))
.Take(limit)
.OrderByDescending(s => s.Created)
.Where(s => s.LibraryId == libraryId)
.AsNoTracking()
.OrderByDescending(s => s.Created)
.Take(limit)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
/// <summary>
@ -325,7 +342,7 @@ namespace API.Data
})
.Where(s => s.AppUserId == userId
&& s.PagesRead > 0
&& s.PagesRead < s.Series.Pages
&& s.PagesRead < (s.Series.Pages - 1) // - 1 because when reading, we start at 0 then go to pages - 1. But when summing, pages assumes starting at 1
&& (libraryId <= 0 || s.Series.LibraryId == libraryId))
.Take(limit)
.OrderByDescending(s => s.LastModified)

View File

@ -12,7 +12,7 @@ namespace API.Entities
/// <summary>
/// Manga Reader Option: How should the image be scaled to screen
/// </summary>
public ScalingOption ScalingOption { get; set; } = ScalingOption.FitToHeight;
public ScalingOption ScalingOption { get; set; } = ScalingOption.Automatic;
/// <summary>
/// Manga Reader Option: Which side of a split image should we show first
/// </summary>

View File

@ -24,6 +24,7 @@ namespace API.Helpers
public static async Task<PagedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize)
{
// NOTE: OrderBy warning being thrown here even if query has the orderby statement
var count = await source.CountAsync();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedList<T>(items, count, pageNumber, pageSize);

View File

@ -58,6 +58,6 @@ namespace API.Interfaces
Task<byte[]> GetVolumeCoverImageAsync(int volumeId);
Task<byte[]> GetSeriesCoverImageAsync(int seriesId);
Task<IEnumerable<SeriesDto>> GetInProgress(int userId, int libraryId, int limit);
Task<IEnumerable<SeriesDto>> GetRecentlyAdded(int libraryId, int limit);
Task<IEnumerable<SeriesDto>> GetRecentlyAdded(int userId, int libraryId, int limit);
}
}

View File

@ -10,5 +10,6 @@
void CleanupChapters(int[] chapterIds);
void RefreshMetadata(int libraryId, bool forceUpdate = true);
void CleanupTemp();
void RefreshSeriesMetadata(int libraryId, int seriesId);
}
}

View File

@ -14,5 +14,11 @@ namespace API.Interfaces.Services
public void UpdateMetadata(Chapter chapter, bool forceUpdate);
public void UpdateMetadata(Volume volume, bool forceUpdate);
public void UpdateMetadata(Series series, bool forceUpdate);
/// <summary>
/// Performs a forced refresh of metatdata just for a series and it's nested entities
/// </summary>
/// <param name="libraryId"></param>
/// <param name="seriesId"></param>
void RefreshMetadataForSeries(int libraryId, int seriesId);
}
}

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using API.Entities.Enums;
using API.Entities.Interfaces;
using API.Interfaces;
using API.Parser;
using ExCSS;
@ -13,7 +12,6 @@ using HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using NetVips;
using VersOne.Epub;
using VersOne.Epub.Schema;
namespace API.Services
{

View File

@ -122,7 +122,7 @@ namespace API.Services
// NOTE: This suffers from code changes not taking effect due to stale data
var firstFile = firstChapter?.Files.FirstOrDefault();
if (firstFile != null &&
(forceUpdate || !firstFile.HasFileBeenModified()))
(forceUpdate || firstFile.HasFileBeenModified())) // !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified)
{
series.Summary = isBook ? _bookService.GetSummaryInfo(firstFile.FilePath) : _archiveService.GetSummaryInfo(firstFile.FilePath);
@ -160,5 +160,38 @@ namespace API.Services
_logger.LogInformation("Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds);
}
}
public void RefreshMetadataForSeries(int libraryId, int seriesId)
{
var sw = Stopwatch.StartNew();
var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult();
var series = library.Series.SingleOrDefault(s => s.Id == seriesId);
if (series == null)
{
_logger.LogError("Series {SeriesId} was not found on Library {LibraryName}", seriesId, libraryId);
return;
}
_logger.LogInformation("Beginning metadata refresh of {SeriesName}", series.Name);
foreach (var volume in series.Volumes)
{
foreach (var chapter in volume.Chapters)
{
UpdateMetadata(chapter, true);
}
UpdateMetadata(volume, true);
}
UpdateMetadata(series, true);
_unitOfWork.SeriesRepository.Update(series);
if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.Complete()).Result)
{
_logger.LogInformation("Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);
}
}
}
}

View File

@ -90,6 +90,12 @@ namespace API.Services
BackgroundJob.Enqueue((() => DirectoryService.ClearDirectory(tempDirectory)));
}
public void RefreshSeriesMetadata(int libraryId, int seriesId)
{
_logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId);
BackgroundJob.Enqueue((() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId)));
}
public void BackupDatabase()
{
BackgroundJob.Enqueue(() => _backupService.BackupDatabase());

View File

@ -9,7 +9,6 @@ using API.Comparators;
using API.Data;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Interfaces;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;