On Deck + Misc Fixes and Changes (#1215)

* Added playwright and started writing e2e tests.

* To make things easy, disabled other browsers while I get confortable. Added a login flow (assumes my dev env)

* More tests on login page

* Lots more testing code, trying to figure out auth code.

* Ensure we don't track DBs inside config

* Added a new date property for when chapters are added to a series which helps with OnDeck calculations. Changed a lot of heavy api calls to use IEnumerable to stream repsonse to UI.

* Fixed OnDeck with a new field for when last chapter was added on Series. This is a streamlined way to query.

Updated Reading List with NormalizedTitle, CoverImage, CoverImageLocked.

* Implemented the ability to read a random item in the reading list and for the reading list to be intact for order.

* Tweaked the style for webtoon to not span the whole width, but use max width

* When we update a cover image just send an event so we don't need to have logic for when updates occur

* Fixed a bad name for entity type on cover updates

* Aligned the edit collection tag modal to align with new tab design

* Rewrote code for picking the first file for metadata to ensure it always picks the correct file, esp if the first chapter of a series starts with a float (1.1)

* Refactored setting LastChapterAdded to ensure we do it on the Series.

* Updated Chapter updating in scan loop to avoid nested for loop and an additional loop.

* Fixed a bug where locked person fields wouldn't persist between scans.

* Updated Contributing to reflect how to view the swagger api
This commit is contained in:
Joseph Milazzo 2022-04-11 17:43:40 -05:00 committed by GitHub
parent 912dfa8a80
commit 3bbb02f574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 3397 additions and 343 deletions

View file

@ -157,7 +157,7 @@ namespace API.Controllers
tag.CoverImageLocked = false;
tag.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, "collectionTag"), false);
MessageFactory.CoverUpdateEvent(tag.Id, "collection"), false);
_unitOfWork.CollectionTagRepository.Update(tag);
}

View file

@ -88,6 +88,22 @@ namespace API.Controllers
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
/// Returns cover image for a Reading List
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet("readinglist-cover")]
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
/// Returns image for a given bookmark page
/// </summary>

View file

@ -7,6 +7,7 @@ using API.DTOs.ReadingLists;
using API.Entities;
using API.Extensions;
using API.Helpers;
using API.SignalR;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
@ -14,11 +15,13 @@ namespace API.Controllers
public class ReadingListController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
public ReadingListController(IUnitOfWork unitOfWork)
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
}
/// <summary>
@ -233,9 +236,12 @@ namespace API.Controllers
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
if (readingList == null) return BadRequest("List does not exist");
if (!string.IsNullOrEmpty(dto.Title))
{
readingList.Title = dto.Title; // Should I check if this is unique?
readingList.NormalizedTitle = Parser.Parser.Normalize(readingList.Title);
}
if (!string.IsNullOrEmpty(dto.Title))
{
@ -244,6 +250,19 @@ namespace API.Controllers
readingList.Promoted = dto.Promoted;
readingList.CoverImageLocked = dto.CoverImageLocked;
if (!dto.CoverImageLocked)
{
readingList.CoverImageLocked = false;
readingList.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, "readingList"), false);
_unitOfWork.ReadingListRepository.Update(readingList);
}
_unitOfWork.ReadingListRepository.Update(readingList);
if (await _unitOfWork.CommitAsync())

View file

@ -214,13 +214,6 @@ namespace API.Controllers
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
}
[HttpPost("recently-added-chapters")]
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChaptersAlt()
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAddedChapters(userId));
}
[HttpPost("all")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{

View file

@ -5,6 +5,7 @@ using API.Data;
using API.DTOs.Uploads;
using API.Extensions;
using API.Services;
using API.SignalR;
using Flurl.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -24,16 +25,18 @@ namespace API.Controllers
private readonly ILogger<UploadController> _logger;
private readonly ITaskScheduler _taskScheduler;
private readonly IDirectoryService _directoryService;
private readonly IEventHub _eventHub;
/// <inheritdoc />
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
ITaskScheduler taskScheduler, IDirectoryService directoryService)
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub)
{
_unitOfWork = unitOfWork;
_imageService = imageService;
_logger = logger;
_taskScheduler = taskScheduler;
_directoryService = directoryService;
_eventHub = eventHub;
}
/// <summary>
@ -145,6 +148,8 @@ namespace API.Controllers
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, "collection"), false);
return Ok();
}
@ -158,6 +163,53 @@ namespace API.Controllers
return BadRequest("Unable to save cover image to Collection Tag");
}
/// <summary>
/// Replaces reading list cover image and locks it with a base64 encoded image
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("reading-list")]
public async Task<ActionResult> UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
return BadRequest("You must pass a url to use");
}
try
{
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
if (!string.IsNullOrEmpty(filePath))
{
readingList.CoverImage = filePath;
readingList.CoverImageLocked = true;
_unitOfWork.ReadingListRepository.Update(readingList);
}
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, "readingList"), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Reading List {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Reading List");
}
/// <summary>
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
/// </summary>

View file

@ -9,5 +9,6 @@
/// Reading lists that are promoted are only done by admins
/// </summary>
public bool Promoted { get; set; }
public bool CoverImageLocked { get; set; }
}
}

View file

@ -6,5 +6,6 @@
public string Title { get; set; }
public string Summary { get; set; }
public bool Promoted { get; set; }
public bool CoverImageLocked { get; set; }
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class SeriesLastChapterAddedAndReadingListNormalization : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastChapterAdded",
table: "Series",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<string>(
name: "CoverImage",
table: "ReadingList",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "CoverImageLocked",
table: "ReadingList",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "NormalizedTitle",
table: "ReadingList",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastChapterAdded",
table: "Series");
migrationBuilder.DropColumn(
name: "CoverImage",
table: "ReadingList");
migrationBuilder.DropColumn(
name: "CoverImageLocked",
table: "ReadingList");
migrationBuilder.DropColumn(
name: "NormalizedTitle",
table: "ReadingList");
}
}
}

View file

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.2");
modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
@ -621,12 +621,21 @@ namespace API.Data.Migrations
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<string>("CoverImage")
.HasColumnType("TEXT");
b.Property<bool>("CoverImageLocked")
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("NormalizedTitle")
.HasColumnType("TEXT");
b.Property<bool>("Promoted")
.HasColumnType("INTEGER");
@ -695,6 +704,9 @@ namespace API.Data.Migrations
b.Property<int>("Format")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastChapterAdded")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");

View file

@ -26,6 +26,7 @@ public interface IReadingListRepository
void BulkRemove(IEnumerable<ReadingListItem> items);
void Update(ReadingList list);
Task<int> Count();
Task<string> GetCoverImageAsync(int readingListId);
}
public class ReadingListRepository : IReadingListRepository
@ -49,6 +50,15 @@ public class ReadingListRepository : IReadingListRepository
return await _context.ReadingList.CountAsync();
}
public async Task<string> GetCoverImageAsync(int readingListId)
{
return await _context.ReadingList
.Where(c => c.Id == readingListId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
public void Remove(ReadingListItem item)
{
_context.ReadingListItem.Remove(item);

View file

@ -95,8 +95,7 @@ public interface ISeriesRepository
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId);
Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId);
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId);
}
public class SeriesRepository : ISeriesRepository
@ -607,7 +606,6 @@ public class SeriesRepository : ISeriesRepository
/// <returns></returns>
public async Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true)
{
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter))
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
new
@ -619,24 +617,19 @@ public class SeriesRepository : ISeriesRepository
LastReadingProgress = _context.AppUserProgresses
.Where(p => p.Id == progress.Id && p.AppUserId == userId)
.Max(p => p.LastModified),
// This is only taking into account chapters that have progress on them, not all chapters in said series
//LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created),
LastChapterCreated = s.Volumes.SelectMany(v => v.Chapters).Max(c => c.Created)
s.LastChapterAdded
});
if (cutoffOnDate)
{
query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint || d.LastChapterCreated >= cutoffProgressPoint);
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint || d.LastChapterAdded >= cutoffProgressPoint);
}
// I think I need another Join statement. The problem is the chapters are still limited to progress
var retSeries = query.Where(s => s.AppUserId == userId
&& s.PagesRead > 0
&& s.PagesRead < s.Series.Pages)
.OrderByDescending(s => s.LastReadingProgress)
.ThenByDescending(s => s.LastChapterCreated)
.ThenByDescending(s => s.LastChapterAdded)
.Select(s => s.Series)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.AsSplitQuery()
@ -903,116 +896,79 @@ public class SeriesRepository : ISeriesRepository
.ToListAsync();
}
private static string RecentlyAddedItemTitle(RecentlyAddedSeries item)
{
switch (item.LibraryType)
{
case LibraryType.Book:
return string.Empty;
case LibraryType.Comic:
return "Issue";
case LibraryType.Manga:
default:
return "Chapter";
}
}
/// <summary>
/// Show all recently added chapters. Provide some mapping for chapter 0 -> Volume 1
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId)
{
var ret = await GetRecentlyAddedChaptersQuery(userId);
var items = new List<RecentlyAddedItemDto>();
foreach (var item in ret)
{
var dto = new RecentlyAddedItemDto()
{
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = items.Count,
Format = item.Format
};
// Add title and Volume/Chapter Id
var chapterTitle = RecentlyAddedItemTitle(item);
string title;
if (item.ChapterNumber.Equals(Parser.Parser.DefaultChapter))
{
if ((item.VolumeNumber + string.Empty).Equals(Parser.Parser.DefaultChapter))
{
title = item.ChapterTitle;
}
else
{
title = "Volume " + item.VolumeNumber;
}
dto.VolumeId = item.VolumeId;
}
else
{
title = item.IsSpecial
? item.ChapterRange
: $"{chapterTitle} {item.ChapterRange}";
dto.ChapterId = item.ChapterId;
}
dto.Title = title;
items.Add(dto);
}
return items;
}
/// <summary>
/// Return recently updated series, regardless of read progress, and group the number of volume or chapters added.
/// </summary>
/// <param name="userId">Used to ensure user has access to libraries</param>
/// <returns></returns>
public async Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId)
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId)
{
var ret = await GetRecentlyAddedChaptersQuery(userId, 150);
var ret = await GetRecentlyAddedChaptersQuery(userId, 150);
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
var index = 0;
foreach (var item in ret)
{
if (seriesMap.ContainsKey(item.SeriesName))
{
seriesMap[item.SeriesName].Count += 1;
}
else
{
seriesMap[item.SeriesName] = new GroupedSeriesDto()
{
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = index,
Format = item.Format,
Count = 1,
};
index += 1;
}
}
return seriesMap.Values.AsEnumerable();
//return seriesMap.Values.ToList();
// var libraries = await _context.AppUser
// .Where(u => u.Id == userId)
// .SelectMany(u => u.Libraries.Select(l => new {LibraryId = l.Id, LibraryType = l.Type}))
// .ToListAsync();
// var libraryIds = libraries.Select(l => l.LibraryId).ToList();
//
// var cuttoffDate = DateTime.Now - TimeSpan.FromDays(12);
//
// var ret2 = _context.Series
// .Where(s => s.LastChapterAdded >= cuttoffDate
// && libraryIds.Contains(s.LibraryId))
// .Select((s) => new GroupedSeriesDto
// {
// LibraryId = s.LibraryId,
// LibraryType = s.Library.Type,
// SeriesId = s.Id,
// SeriesName = s.Name,
// //Created = s.LastChapterAdded, // Hmm on first migration this wont work
// Created = s.Volumes.SelectMany(v => v.Chapters).Max(c => c.Created), // Hmm on first migration this wont work
// Count = s.Volumes.SelectMany(v => v.Chapters).Count(c => c.Created >= cuttoffDate),
// //Id = index,
// Format = s.Format
// })
// .Take(50)
// .OrderByDescending(c => c.Created)
// .AsSplitQuery()
// .AsEnumerable();
//
// return ret2;
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
var index = 0;
foreach (var item in ret)
{
if (seriesMap.ContainsKey(item.SeriesName))
{
seriesMap[item.SeriesName].Count += 1;
}
else
{
seriesMap[item.SeriesName] = new GroupedSeriesDto()
{
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = index,
Format = item.Format,
Count = 1
};
index += 1;
}
}
return seriesMap.Values.ToList();
}
private async Task<List<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 50)
private async Task<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 50)
{
var libraries = await _context.AppUser
.Where(u => u.Id == userId)
@ -1021,7 +977,7 @@ public class SeriesRepository : ISeriesRepository
var libraryIds = libraries.Select(l => l.LibraryId).ToList();
var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12);
var ret = await _context.Chapter
var ret = _context.Chapter
.Where(c => c.Created >= withinLastWeek)
.AsNoTracking()
.Include(c => c.Volume)
@ -1045,8 +1001,9 @@ public class SeriesRepository : ISeriesRepository
ChapterTitle = c.Title
})
.Take(maxRecords)
.AsSplitQuery()
.Where(c => c.Created >= withinLastWeek && libraryIds.Contains(c.LibraryId))
.ToListAsync();
.AsEnumerable();
return ret;
}
}

View file

@ -11,11 +11,21 @@ namespace API.Entities
{
public int Id { get; init; }
public string Title { get; set; }
/// <summary>
/// A normalized string used to check if the reading list already exists in the DB
/// </summary>
public string NormalizedTitle { get; set; }
public string Summary { get; set; }
/// <summary>
/// Reading lists that are promoted are only done by admins
/// </summary>
public bool Promoted { get; set; }
/// <summary>
/// Absolute path to the (managed) image file
/// </summary>
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
public string CoverImage { get; set; }
public bool CoverImageLocked { get; set; }
public ICollection<ReadingListItem> Items { get; set; }
public DateTime Created { get; set; }

View file

@ -62,6 +62,11 @@ namespace API.Entities
public bool SortNameLocked { get; set; }
public bool LocalizedNameLocked { get; set; }
/// <summary>
/// When a Chapter was last added onto the Series
/// </summary>
public DateTime LastChapterAdded { get; set; }
public SeriesMetadata Metadata { get; set; }
public ICollection<AppUserRating> Ratings { get; set; } = new List<AppUserRating>();
public ICollection<AppUserProgress> Progress { get; set; } = new List<AppUserProgress>();

View file

@ -8,14 +8,6 @@ namespace API.Extensions
{
public static class VolumeListExtensions
{
public static Volume FirstWithChapters(this IEnumerable<Volume> volumes, bool inBookSeries)
{
return inBookSeries
? volumes.FirstOrDefault(v => v.Chapters.Any())
: volumes.OrderBy(v => v.Number, new ChapterSortComparer())
.FirstOrDefault(v => v.Chapters.Any());
}
/// <summary>
/// Selects the first Volume to get the cover image from. For a book with only a special, the special will be returned.
/// If there are both specials and non-specials, then the first non-special will be returned.

View file

@ -64,16 +64,14 @@ public static class PersonHelper
/// </summary>
/// <param name="existingPeople"></param>
/// <param name="removeAllExcept"></param>
/// <param name="action">Callback for all entities that was removed</param>
public static void KeepOnlySamePeopleBetweenLists(ICollection<Person> existingPeople, ICollection<Person> removeAllExcept, Action<Person> action = null)
/// <param name="action">Callback for all entities that should be removed</param>
public static void KeepOnlySamePeopleBetweenLists(IEnumerable<Person> existingPeople, ICollection<Person> removeAllExcept, Action<Person> action = null)
{
var existing = existingPeople.ToList();
foreach (var person in existing)
foreach (var person in existingPeople)
{
var existingPerson = removeAllExcept.FirstOrDefault(p => p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName));
if (existingPerson == null)
{
existingPeople.Remove(person);
action?.Invoke(person);
}
}

View file

@ -143,4 +143,16 @@ public class ImageService : IImageService
{
return $"tag{tagId}";
}
/// <summary>
/// Returns the name format for a reading list cover image
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
public static string GetReadingListFormat(int readingListId)
{
return $"readinglist{readingListId}";
}
}

View file

@ -33,7 +33,7 @@ public class ReadingItemService : IReadingItemService
}
/// <summary>
/// Gets the ComicInfo for the file if it exists. Null otherewise.
/// Gets the ComicInfo for the file if it exists. Null otherwise.
/// </summary>
/// <param name="filePath">Fully qualified path of file</param>
/// <returns></returns>

View file

@ -10,6 +10,7 @@ using API.DTOs.CollectionTags;
using API.DTOs.Metadata;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers;
using API.SignalR;
using Microsoft.Extensions.Logging;
@ -41,6 +42,19 @@ public class SeriesService : ISeriesService
_logger = logger;
}
/// <summary>
/// Returns the first chapter for a series to extract metadata from (ie Summary, etc)
/// </summary>
/// <param name="series"></param>
/// <param name="isBookLibrary"></param>
/// <returns></returns>
public static Chapter GetFirstChapterForMetadata(Series series, bool isBookLibrary)
{
return series.Volumes.OrderBy(v => v.Number, new ChapterSortComparer())
.SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer()))
.FirstOrDefault();
}
public async Task<bool> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
{
try
@ -154,6 +168,8 @@ public class SeriesService : ISeriesService
if (!updateSeriesMetadataDto.SeriesMetadata.WriterLocked) series.Metadata.WriterLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.SummaryLocked) series.Metadata.SummaryLocked = false;
series.Metadata.PublisherLocked = updateSeriesMetadataDto.SeriesMetadata.PublisherLocked;
if (!_unitOfWork.HasChanges())
@ -334,7 +350,7 @@ public class SeriesService : ISeriesService
var existingTag = allTags.SingleOrDefault(t => t.Name == tag.Name && t.Role == tag.Role);
if (existingTag != null)
{
if (series.Metadata.People.All(t => t.Name != tag.Name && t.Role == tag.Role))
if (series.Metadata.People.Where(t => t.Role == tag.Role).All(t => !t.Name.Equals(tag.Name)))
{
handleAdd(existingTag);
isModified = true;

View file

@ -564,8 +564,7 @@ public class ScannerService : IScannerService
private static void UpdateSeriesMetadata(Series series, ICollection<Person> allPeople, ICollection<Genre> allGenres, ICollection<Tag> allTags, LibraryType libraryType)
{
var isBook = libraryType == LibraryType.Book;
var firstVolume = series.Volumes.OrderBy(c => c.Number, new ChapterSortComparer()).FirstWithChapters(isBook);
var firstChapter = firstVolume?.Chapters.GetFirstChapterWithFiles();
var firstChapter = SeriesService.GetFirstChapterForMetadata(series, isBook);
var firstFile = firstChapter?.Files.FirstOrDefault();
if (firstFile == null) return;
@ -695,9 +694,50 @@ public class ScannerService : IScannerService
}
}
// BUG: The issue here is that people is just from chapter, but series metadata might already have some people on it
// I might be able to filter out people that are in locked fields?
var people = chapters.SelectMany(c => c.People).ToList();
PersonHelper.KeepOnlySamePeopleBetweenLists(series.Metadata.People,
people, person => series.Metadata.People.Remove(person));
people, person =>
{
switch (person.Role)
{
case PersonRole.Writer:
if (!series.Metadata.WriterLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Penciller:
if (!series.Metadata.PencillerLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Inker:
if (!series.Metadata.InkerLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Colorist:
if (!series.Metadata.ColoristLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Letterer:
if (!series.Metadata.LettererLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.CoverArtist:
if (!series.Metadata.CoverArtistLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Editor:
if (!series.Metadata.EditorLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Publisher:
if (!series.Metadata.PublisherLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Character:
if (!series.Metadata.CharacterLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Translator:
if (!series.Metadata.TranslatorLocked) series.Metadata.People.Remove(person);
break;
case PersonRole.Other:
default:
series.Metadata.People.Remove(person);
break;
}
});
}
@ -720,7 +760,7 @@ public class ScannerService : IScannerService
_logger.LogDebug("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name);
var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray();
UpdateChapters(volume, infos);
UpdateChapters(series, volume, infos);
volume.Pages = volume.Chapters.Sum(c => c.Pages);
// Update all the metadata on the Chapters
@ -767,7 +807,7 @@ public class ScannerService : IScannerService
series.Name, startingVolumeCount, series.Volumes.Count);
}
private void UpdateChapters(Volume volume, IList<ParserInfo> parsedInfos)
private void UpdateChapters(Series series, Volume volume, IList<ParserInfo> parsedInfos)
{
// Add new chapters
foreach (var info in parsedInfos)
@ -789,30 +829,18 @@ public class ScannerService : IScannerService
{
_logger.LogDebug(
"[ScannerService] Adding new chapter, {Series} - Vol {Volume} Ch {Chapter}", info.Series, info.Volumes, info.Chapters);
volume.Chapters.Add(DbFactory.Chapter(info));
chapter = DbFactory.Chapter(info);
volume.Chapters.Add(chapter);
series.LastChapterAdded = DateTime.Now;
}
else
{
chapter.UpdateFrom(info);
}
}
// Add files
foreach (var info in parsedInfos)
{
var specialTreatment = info.IsSpecialInfo();
Chapter chapter;
try
{
chapter = volume.Chapters.GetChapterByRange(info);
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception parsing chapter. Skipping {SeriesName} Vol {VolumeNumber} Chapter {ChapterNumber} - Special treatment: {NeedsSpecialTreatment}", info.Series, volume.Name, info.Chapters, specialTreatment);
continue;
}
if (chapter == null) continue;
// Add files
var specialTreatment = info.IsSpecialInfo();
AddOrUpdateFileForChapter(chapter, info);
chapter.Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty;
chapter.Range = specialTreatment ? info.Filename : info.Chapters;

Binary file not shown.

Binary file not shown.