diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index b7589e6dd..9cc7bd65e 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -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($"")); } @@ -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); } diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 94a5b024a..d0d47cb5d 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -145,7 +145,8 @@ namespace API.Controllers [HttpGet("recently-added")] public async Task>> 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(); + } } } \ No newline at end of file diff --git a/API/DTOs/RefreshSeriesDto.cs b/API/DTOs/RefreshSeriesDto.cs new file mode 100644 index 000000000..bc6344ea2 --- /dev/null +++ b/API/DTOs/RefreshSeriesDto.cs @@ -0,0 +1,8 @@ +namespace API.DTOs +{ + public class RefreshSeriesDto + { + public int LibraryId { get; set; } + public int SeriesId { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 3cb7f120b..0f8f4263c 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -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. /// public string UserReview { get; set; } + + public DateTime Created { get; set; } public int LibraryId { get; set; } public string LibraryName { get; set; } diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index de35a8321..9250d592a 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -80,15 +80,12 @@ namespace API.Data public async Task> 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(_mapper.ConfigurationProvider) .AsNoTracking(); - - _logger.LogDebug("Processed GetSeriesDtoForLibraryIdAsync in {ElapsedMilliseconds} milliseconds", sw.ElapsedMilliseconds); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } @@ -295,15 +292,35 @@ namespace API.Data /// Library to restrict to, if 0, will apply to all libraries /// How many series to pick. /// - public async Task> GetRecentlyAdded(int libraryId, int limit) + public async Task> 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(_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(_mapper.ConfigurationProvider) .ToListAsync(); + + } /// @@ -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) diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index a4a773a38..4671c2692 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -12,7 +12,7 @@ namespace API.Entities /// /// Manga Reader Option: How should the image be scaled to screen /// - public ScalingOption ScalingOption { get; set; } = ScalingOption.FitToHeight; + public ScalingOption ScalingOption { get; set; } = ScalingOption.Automatic; /// /// Manga Reader Option: Which side of a split image should we show first /// diff --git a/API/Helpers/PagedList.cs b/API/Helpers/PagedList.cs index 0900f02a5..b87687a6e 100644 --- a/API/Helpers/PagedList.cs +++ b/API/Helpers/PagedList.cs @@ -24,6 +24,7 @@ namespace API.Helpers public static async Task> CreateAsync(IQueryable 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(items, count, pageNumber, pageSize); diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index be39e10d4..eff8e7c08 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -58,6 +58,6 @@ namespace API.Interfaces Task GetVolumeCoverImageAsync(int volumeId); Task GetSeriesCoverImageAsync(int seriesId); Task> GetInProgress(int userId, int libraryId, int limit); - Task> GetRecentlyAdded(int libraryId, int limit); + Task> GetRecentlyAdded(int userId, int libraryId, int limit); } } \ No newline at end of file diff --git a/API/Interfaces/ITaskScheduler.cs b/API/Interfaces/ITaskScheduler.cs index 5de2f6941..75f70c1fa 100644 --- a/API/Interfaces/ITaskScheduler.cs +++ b/API/Interfaces/ITaskScheduler.cs @@ -10,5 +10,6 @@ void CleanupChapters(int[] chapterIds); void RefreshMetadata(int libraryId, bool forceUpdate = true); void CleanupTemp(); + void RefreshSeriesMetadata(int libraryId, int seriesId); } } \ No newline at end of file diff --git a/API/Interfaces/Services/IMetadataService.cs b/API/Interfaces/Services/IMetadataService.cs index 830cab1eb..70b10b861 100644 --- a/API/Interfaces/Services/IMetadataService.cs +++ b/API/Interfaces/Services/IMetadataService.cs @@ -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); + /// + /// Performs a forced refresh of metatdata just for a series and it's nested entities + /// + /// + /// + void RefreshMetadataForSeries(int libraryId, int seriesId); } } \ No newline at end of file diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index e09485645..2dfbd4798 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -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 { diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 5e1f125bb..8d479ed90 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -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); + } + } } } \ No newline at end of file diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 8857865c0..b284fd9f7 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -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()); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index f558c3a3a..726e31dc3 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -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;