UX Overhaul Part 2 (#3112)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
0247bc5012
commit
3d8aa2ad24
192 changed files with 14808 additions and 1874 deletions
|
@ -1,20 +1,40 @@
|
|||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.SignalR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Nager.ArticleNumber;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class ChapterController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
public ChapterController(IUnitOfWork unitOfWork)
|
||||
public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ChapterDto>> GetChapter(int chapterId)
|
||||
{
|
||||
|
@ -25,5 +45,234 @@ public class ChapterController : BaseApiController
|
|||
return Ok(chapter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a Chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult<bool>> DeleteChapter(int chapterId)
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null)
|
||||
return BadRequest(_localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
|
||||
var vol = (await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId))!;
|
||||
_unitOfWork.ChapterRepository.Remove(chapter);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, MessageFactory.ChapterRemovedEvent(chapter.Id, vol.SeriesId), false);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update chapter metadata
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateChapterMetadata(UpdateChapterDto dto)
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.Id, ChapterIncludes.People | ChapterIncludes.Genres | ChapterIncludes.Tags);
|
||||
if (chapter == null)
|
||||
return BadRequest(_localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
|
||||
if (chapter.AgeRating != dto.AgeRating)
|
||||
{
|
||||
chapter.AgeRating = dto.AgeRating;
|
||||
}
|
||||
|
||||
|
||||
if (chapter.Summary != dto.Summary.Trim())
|
||||
{
|
||||
chapter.Summary = dto.Summary.Trim();
|
||||
}
|
||||
|
||||
if (chapter.Language != dto.Language)
|
||||
{
|
||||
chapter.Language = dto.Language ?? string.Empty;
|
||||
}
|
||||
|
||||
if (chapter.SortOrder.IsNot(dto.SortOrder))
|
||||
{
|
||||
chapter.SortOrder = dto.SortOrder; // TODO: Figure out validation
|
||||
}
|
||||
|
||||
if (chapter.TitleName != dto.TitleName)
|
||||
{
|
||||
chapter.TitleName = dto.TitleName;
|
||||
}
|
||||
|
||||
if (chapter.ReleaseDate != dto.ReleaseDate)
|
||||
{
|
||||
chapter.ReleaseDate = dto.ReleaseDate;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.ISBN) && ArticleNumberHelper.IsValidIsbn10(dto.ISBN) ||
|
||||
ArticleNumberHelper.IsValidIsbn13(dto.ISBN))
|
||||
{
|
||||
chapter.ISBN = dto.ISBN;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(dto.WebLinks))
|
||||
{
|
||||
chapter.WebLinks = string.Empty;
|
||||
} else
|
||||
{
|
||||
chapter.WebLinks = string.Join(',', dto.WebLinks
|
||||
.Split(',')
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Select(s => s.Trim())!
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#region Genres
|
||||
if (dto.Genres != null &&
|
||||
dto.Genres.Count != 0)
|
||||
{
|
||||
var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(dto.Genres.Select(t => Parser.Normalize(t.Title)))).ToList();
|
||||
chapter.Genres ??= new List<Genre>();
|
||||
GenreHelper.UpdateGenreList(dto.Genres, chapter, allGenres, genre =>
|
||||
{
|
||||
chapter.Genres.Add(genre);
|
||||
}, () => chapter.GenresLocked = true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Tags
|
||||
if (dto.Tags is {Count: > 0})
|
||||
{
|
||||
var allTags = (await _unitOfWork.TagRepository
|
||||
.GetAllTagsByNameAsync(dto.Tags.Select(t => Parser.Normalize(t.Title))))
|
||||
.ToList();
|
||||
chapter.Tags ??= new List<Tag>();
|
||||
TagHelper.UpdateTagList(dto.Tags, chapter, allTags, tag =>
|
||||
{
|
||||
chapter.Tags.Add(tag);
|
||||
}, () => chapter.TagsLocked = true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region People
|
||||
if (PersonHelper.HasAnyPeople(dto))
|
||||
{
|
||||
void HandleAddPerson(Person person)
|
||||
{
|
||||
PersonHelper.AddPersonIfNotExists(chapter.People, person);
|
||||
}
|
||||
|
||||
chapter.People ??= new List<Person>();
|
||||
var allWriters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Writer,
|
||||
dto.Writers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Writer, dto.Writers, chapter, allWriters.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.WriterLocked = true);
|
||||
|
||||
var allCharacters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Character,
|
||||
dto.Characters.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Character, dto.Characters, chapter, allCharacters.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.CharacterLocked = true);
|
||||
|
||||
var allColorists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Colorist,
|
||||
dto.Colorists.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Colorist, dto.Colorists, chapter, allColorists.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.ColoristLocked = true);
|
||||
|
||||
var allEditors = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Editor,
|
||||
dto.Editors.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Editor, dto.Editors, chapter, allEditors.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.EditorLocked = true);
|
||||
|
||||
var allInkers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Inker,
|
||||
dto.Inkers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Inker, dto.Inkers, chapter, allInkers.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.InkerLocked = true);
|
||||
|
||||
var allLetterers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Letterer,
|
||||
dto.Letterers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Letterer, dto.Letterers, chapter, allLetterers.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.LettererLocked = true);
|
||||
|
||||
var allPencillers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Penciller,
|
||||
dto.Pencillers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Penciller, dto.Pencillers, chapter, allPencillers.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.PencillerLocked = true);
|
||||
|
||||
var allPublishers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Publisher,
|
||||
dto.Publishers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Publisher, dto.Publishers, chapter, allPublishers.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.PublisherLocked = true);
|
||||
|
||||
var allImprints = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Imprint,
|
||||
dto.Imprints.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Imprint, dto.Imprints, chapter, allImprints.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.ImprintLocked = true);
|
||||
|
||||
var allTeams = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Team,
|
||||
dto.Imprints.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Team, dto.Teams, chapter, allTeams.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.TeamLocked = true);
|
||||
|
||||
var allLocations = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Location,
|
||||
dto.Imprints.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Location, dto.Locations, chapter, allLocations.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.LocationLocked = true);
|
||||
|
||||
var allTranslators = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Translator,
|
||||
dto.Translators.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Translator, dto.Translators, chapter, allTranslators.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.TranslatorLocked = true);
|
||||
|
||||
var allCoverArtists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.CoverArtist,
|
||||
dto.CoverArtists.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, dto.CoverArtists, chapter, allCoverArtists.AsReadOnly(),
|
||||
HandleAddPerson, () => chapter.CoverArtistLocked = true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Locks
|
||||
chapter.AgeRatingLocked = dto.AgeRatingLocked;
|
||||
chapter.LanguageLocked = dto.LanguageLocked;
|
||||
chapter.TitleNameLocked = dto.TitleNameLocked;
|
||||
chapter.SortOrderLocked = dto.SortOrderLocked;
|
||||
chapter.GenresLocked = dto.GenresLocked;
|
||||
chapter.TagsLocked = dto.TagsLocked;
|
||||
chapter.CharacterLocked = dto.CharacterLocked;
|
||||
chapter.ColoristLocked = dto.ColoristLocked;
|
||||
chapter.EditorLocked = dto.EditorLocked;
|
||||
chapter.InkerLocked = dto.InkerLocked;
|
||||
chapter.ImprintLocked = dto.ImprintLocked;
|
||||
chapter.LettererLocked = dto.LettererLocked;
|
||||
chapter.PencillerLocked = dto.PencillerLocked;
|
||||
chapter.PublisherLocked = dto.PublisherLocked;
|
||||
chapter.TranslatorLocked = dto.TranslatorLocked;
|
||||
chapter.CoverArtistLocked = dto.CoverArtistLocked;
|
||||
chapter.WriterLocked = dto.WriterLocked;
|
||||
chapter.SummaryLocked = dto.SummaryLocked;
|
||||
chapter.ISBNLocked = dto.ISBNLocked;
|
||||
chapter.ReleaseDateLocked = dto.ReleaseDateLocked;
|
||||
#endregion
|
||||
|
||||
|
||||
if (!_unitOfWork.HasChanges())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// TODO: Emit a ChapterMetadataUpdate out
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ public class ReadingListController : BaseApiController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Reading Lists the user has access to that have a series within it.
|
||||
/// Returns all Reading Lists the user has access to that the given series within it.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
|
@ -74,6 +74,18 @@ public class ReadingListController : BaseApiController
|
|||
seriesId, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Reading Lists the user has access to that has the given chapter within it.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("lists-for-chapter")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForChapter(int chapterId)
|
||||
{
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForChapterAndUserAsync(User.GetUserId(),
|
||||
chapterId, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress
|
||||
/// </summary>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
|
@ -92,28 +93,36 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
// 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(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
|
||||
|
||||
var filePath = string.Empty;
|
||||
var lockState = false;
|
||||
if (!string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
|
||||
lockState = uploadFileDto.LockCover;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
series.CoverImage = filePath;
|
||||
series.CoverImageLocked = true;
|
||||
series.CoverImageLocked = lockState;
|
||||
_imageService.UpdateColorScape(series);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
// Refresh covers
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -142,25 +151,24 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
// 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(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(uploadFileDto.Id);
|
||||
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
var filePath = string.Empty;
|
||||
var lockState = false;
|
||||
if (!string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
tag.CoverImage = filePath;
|
||||
tag.CoverImageLocked = true;
|
||||
_imageService.UpdateColorScape(tag);
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
lockState = uploadFileDto.LockCover;
|
||||
}
|
||||
|
||||
tag.CoverImage = filePath;
|
||||
tag.CoverImageLocked = lockState;
|
||||
_imageService.UpdateColorScape(tag);
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -189,30 +197,31 @@ public class UploadController : BaseApiController
|
|||
[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.
|
||||
// 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(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
if (_readingListService.UserHasReadingListAccess(uploadFileDto.Id, User.GetUsername()) == null)
|
||||
if (await _readingListService.UserHasReadingListAccess(uploadFileDto.Id, User.GetUsername()) == null)
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "access-denied"));
|
||||
|
||||
try
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
||||
var filePath = string.Empty;
|
||||
var lockState = false;
|
||||
if (!string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
readingList.CoverImage = filePath;
|
||||
readingList.CoverImageLocked = true;
|
||||
_imageService.UpdateColorScape(readingList);
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
lockState = uploadFileDto.LockCover;
|
||||
}
|
||||
|
||||
|
||||
readingList.CoverImage = filePath;
|
||||
readingList.CoverImageLocked = lockState;
|
||||
_imageService.UpdateColorScape(readingList);
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -253,33 +262,42 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
// 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(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
var filePath = string.Empty;
|
||||
var lockState = false;
|
||||
if (!string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
chapter.CoverImage = filePath;
|
||||
chapter.CoverImageLocked = true;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
if (volume != null)
|
||||
{
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
}
|
||||
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
lockState = uploadFileDto.LockCover;
|
||||
}
|
||||
|
||||
chapter.CoverImage = filePath;
|
||||
chapter.CoverImageLocked = lockState;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
if (volume != null)
|
||||
{
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
volume.CoverImageLocked = lockState;
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Refresh covers
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId))!;
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
|
||||
}
|
||||
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
|
@ -310,11 +328,6 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
// 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(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(uploadFileDto.Id, VolumeIncludes.Chapters);
|
||||
|
@ -323,24 +336,37 @@ public class UploadController : BaseApiController
|
|||
// Find the first chapter of the volume
|
||||
var chapter = volume.Chapters[0];
|
||||
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(chapter.Id, uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
var filePath = string.Empty;
|
||||
var lockState = false;
|
||||
if (!string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
chapter.CoverImage = filePath;
|
||||
chapter.CoverImageLocked = true;
|
||||
_imageService.UpdateColorScape(chapter);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_imageService.UpdateColorScape(volume);
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(chapter.Id, uploadFileDto.Id)}");
|
||||
lockState = uploadFileDto.LockCover;
|
||||
}
|
||||
|
||||
|
||||
chapter.CoverImage = filePath;
|
||||
chapter.CoverImageLocked = lockState;
|
||||
_imageService.UpdateColorScape(chapter);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
volume.CoverImageLocked = lockState;
|
||||
_imageService.UpdateColorScape(volume);
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Refresh covers
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!;
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
|
||||
}
|
||||
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
|
@ -426,6 +452,7 @@ public class UploadController : BaseApiController
|
|||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset-chapter-lock")]
|
||||
[Obsolete("Use LockCover in UploadFileDto")]
|
||||
public async Task<ActionResult> ResetChapterLock(UploadFileDto uploadFileDto)
|
||||
{
|
||||
try
|
||||
|
@ -461,4 +488,6 @@ public class UploadController : BaseApiController
|
|||
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
54
API/Controllers/VolumeController.cs
Normal file
54
API/Controllers/VolumeController.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class VolumeController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
public VolumeController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
|
||||
{
|
||||
var volume =
|
||||
await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, User.GetUserId());
|
||||
|
||||
return Ok(volume);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult<bool>> DeleteVolume(int volumeId)
|
||||
{
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId,
|
||||
VolumeIncludes.Chapters | VolumeIncludes.People | VolumeIncludes.Tags);
|
||||
if (volume == null)
|
||||
return BadRequest(_localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
|
||||
_unitOfWork.VolumeRepository.Remove(volume);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
|
@ -158,6 +158,33 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage
|
|||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
public bool LanguageLocked { get; set; }
|
||||
public bool SummaryLocked { get; set; }
|
||||
/// <summary>
|
||||
/// Locked by user so metadata updates from scan loop will not override AgeRating
|
||||
/// </summary>
|
||||
public bool AgeRatingLocked { get; set; }
|
||||
/// <summary>
|
||||
/// Locked by user so metadata updates from scan loop will not override PublicationStatus
|
||||
/// </summary>
|
||||
public bool PublicationStatusLocked { get; set; }
|
||||
public bool GenresLocked { get; set; }
|
||||
public bool TagsLocked { get; set; }
|
||||
public bool WriterLocked { get; set; }
|
||||
public bool CharacterLocked { get; set; }
|
||||
public bool ColoristLocked { get; set; }
|
||||
public bool EditorLocked { get; set; }
|
||||
public bool InkerLocked { get; set; }
|
||||
public bool ImprintLocked { get; set; }
|
||||
public bool LettererLocked { get; set; }
|
||||
public bool PencillerLocked { get; set; }
|
||||
public bool PublisherLocked { get; set; }
|
||||
public bool TranslatorLocked { get; set; }
|
||||
public bool TeamLocked { get; set; }
|
||||
public bool LocationLocked { get; set; }
|
||||
public bool CoverArtistLocked { get; set; }
|
||||
public bool ReleaseYearLocked { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public string CoverImage { get; set; }
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Metadata;
|
||||
|
@ -7,6 +8,7 @@ namespace API.DTOs.Metadata;
|
|||
/// <summary>
|
||||
/// Exclusively metadata about a given chapter
|
||||
/// </summary>
|
||||
[Obsolete("Will not be maintained as of v0.8.1")]
|
||||
public class ChapterMetadataDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
94
API/DTOs/UpdateChapterDto.cs
Normal file
94
API/DTOs/UpdateChapterDto.cs
Normal file
|
@ -0,0 +1,94 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.DTOs.Metadata;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs;
|
||||
|
||||
public class UpdateChapterDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Genres for the Chapter
|
||||
/// </summary>
|
||||
public ICollection<GenreTagDto> Genres { get; set; } = new List<GenreTagDto>();
|
||||
/// <summary>
|
||||
/// Collection of all Tags from underlying chapters for a Chapter
|
||||
/// </summary>
|
||||
public ICollection<TagDto> Tags { get; set; } = new List<TagDto>();
|
||||
|
||||
public ICollection<PersonDto> Writers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> CoverArtists { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Publishers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Imprints { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Teams { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Locations { get; set; } = new List<PersonDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Highest Age Rating from all Chapters
|
||||
/// </summary>
|
||||
public AgeRating AgeRating { get; set; } = AgeRating.Unknown;
|
||||
/// <summary>
|
||||
/// Language of the content (BCP-47 code)
|
||||
/// </summary>
|
||||
public string Language { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Locked by user so metadata updates from scan loop will not override AgeRating
|
||||
/// </summary>
|
||||
public bool AgeRatingLocked { get; set; }
|
||||
public bool TitleNameLocked { get; set; }
|
||||
public bool GenresLocked { get; set; }
|
||||
public bool TagsLocked { get; set; }
|
||||
public bool WriterLocked { get; set; }
|
||||
public bool CharacterLocked { get; set; }
|
||||
public bool ColoristLocked { get; set; }
|
||||
public bool EditorLocked { get; set; }
|
||||
public bool InkerLocked { get; set; }
|
||||
public bool ImprintLocked { get; set; }
|
||||
public bool LettererLocked { get; set; }
|
||||
public bool PencillerLocked { get; set; }
|
||||
public bool PublisherLocked { get; set; }
|
||||
public bool TranslatorLocked { get; set; }
|
||||
public bool TeamLocked { get; set; }
|
||||
public bool LocationLocked { get; set; }
|
||||
public bool CoverArtistLocked { get; set; }
|
||||
public bool LanguageLocked { get; set; }
|
||||
public bool SummaryLocked { get; set; }
|
||||
public bool ISBNLocked { get; set; }
|
||||
public bool ReleaseDateLocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.
|
||||
/// </summary>
|
||||
public float SortOrder { get; set; }
|
||||
/// <summary>
|
||||
/// Can the sort order be updated on scan or is it locked from UI
|
||||
/// </summary>
|
||||
public bool SortOrderLocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated link of urls to external services that have some relation to the Chapter
|
||||
/// </summary>
|
||||
public string WebLinks { get; set; } = string.Empty;
|
||||
public string ISBN { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Date which chapter was released
|
||||
/// </summary>
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
/// <summary>
|
||||
/// Chapter title
|
||||
/// </summary>
|
||||
/// <remarks>This should not be confused with Title which is used for special filenames.</remarks>
|
||||
public string TitleName { get; set; } = string.Empty;
|
||||
}
|
|
@ -10,4 +10,9 @@ public class UploadFileDto
|
|||
/// Base Url encoding of the file to upload from (can be null)
|
||||
/// </summary>
|
||||
public required string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Lock the cover or not
|
||||
/// </summary>
|
||||
public bool LockCover { get; set; } = true;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage
|
|||
public int MaxHoursToRead { get; set; }
|
||||
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
||||
public int AvgHoursToRead { get; set; }
|
||||
public long WordCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this a loose leaf volume
|
||||
|
@ -64,6 +65,7 @@ public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage
|
|||
}
|
||||
|
||||
public string CoverImage { get; set; }
|
||||
private bool CoverImageLocked { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
|
||||
|
|
3142
API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs
generated
Normal file
3142
API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
249
API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs
Normal file
249
API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs
Normal file
|
@ -0,0 +1,249 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChapterMetadataLocks : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AgeRatingLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CharacterLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ColoristLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CoverArtistLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "EditorLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "GenresLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ISBNLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ImprintLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "InkerLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "LanguageLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "LettererLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "LocationLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "PencillerLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "PublisherLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ReleaseDateLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SummaryLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "TagsLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "TeamLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "TitleNameLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "TranslatorLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "WriterLocked",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AgeRatingLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CharacterLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ColoristLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverArtistLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EditorLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GenresLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ISBNLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ImprintLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "InkerLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LanguageLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LettererLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LocationLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PencillerLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PublisherLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ReleaseDateLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SummaryLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TagsLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TeamLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TitleNameLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TranslatorLocked",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WriterLocked",
|
||||
table: "Chapter");
|
||||
}
|
||||
}
|
||||
}
|
3145
API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs
generated
Normal file
3145
API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
29
API/Data/Migrations/20240813194728_VolumeCoverLocked.cs
Normal file
29
API/Data/Migrations/20240813194728_VolumeCoverLocked.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class VolumeCoverLocked : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CoverImageLocked",
|
||||
table: "Volume",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImageLocked",
|
||||
table: "Volume");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -719,6 +719,9 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("AgeRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AgeRatingLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AlternateCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -731,9 +734,18 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("AvgHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("CharacterLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ColoristLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("CoverArtistLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -746,23 +758,47 @@ namespace API.Data.Migrations
|
|||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EditorLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("GenresLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ISBN")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<bool>("ISBNLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ImprintLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("InkerLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSpecial")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("LanguageLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModifiedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("LettererLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LocationLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -781,15 +817,24 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("PencillerLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PublisherLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Range")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ReleaseDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ReleaseDateLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecondaryColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -811,15 +856,30 @@ namespace API.Data.Migrations
|
|||
b.Property<string>("Summary")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("SummaryLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TagsLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TeamLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TitleName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TitleNameLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TotalCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TranslatorLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VolumeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -831,6 +891,9 @@ namespace API.Data.Migrations
|
|||
b.Property<long>("WordCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WriterLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("VolumeId");
|
||||
|
@ -1983,6 +2046,9 @@ namespace API.Data.Migrations
|
|||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CoverImageLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
|
@ -22,12 +22,15 @@ public enum ChapterIncludes
|
|||
None = 1,
|
||||
Volumes = 2,
|
||||
Files = 4,
|
||||
People = 8
|
||||
People = 8,
|
||||
Genres = 16,
|
||||
Tags = 32
|
||||
}
|
||||
|
||||
public interface IChapterRepository
|
||||
{
|
||||
void Update(Chapter chapter);
|
||||
void Remove(Chapter chapter);
|
||||
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds, ChapterIncludes includes = ChapterIncludes.None);
|
||||
Task<IChapterInfoDto?> GetChapterInfoDtoAsync(int chapterId);
|
||||
Task<int> GetChapterTotalPagesAsync(int chapterId);
|
||||
|
@ -60,6 +63,11 @@ public class ChapterRepository : IChapterRepository
|
|||
_context.Entry(chapter).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Remove(Chapter chapter)
|
||||
{
|
||||
_context.Chapter.Remove(chapter);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds, ChapterIncludes includes = ChapterIncludes.None)
|
||||
{
|
||||
return await _context.Chapter
|
||||
|
|
|
@ -36,6 +36,8 @@ public interface IReadingListRepository
|
|||
Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
|
||||
Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId,
|
||||
bool includePromoted);
|
||||
Task<IEnumerable<ReadingListDto>> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId,
|
||||
bool includePromoted);
|
||||
void Remove(ReadingListItem item);
|
||||
void Add(ReadingList list);
|
||||
void BulkRemove(IEnumerable<ReadingListItem> items);
|
||||
|
@ -166,6 +168,8 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void Remove(ReadingListItem item)
|
||||
{
|
||||
_context.ReadingListItem.Remove(item);
|
||||
|
@ -204,6 +208,19 @@ public class ReadingListRepository : IReadingListRepository
|
|||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReadingListDto>> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, bool includePromoted)
|
||||
{
|
||||
var query = _context.ReadingList
|
||||
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
|
||||
.Where(l => l.Items.Any(i => i.ChapterId == chapterId))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(l => l.Title)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking();
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ReadingList?> GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Extensions;
|
||||
|
@ -125,6 +124,32 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
|||
public string WebLinks { get; set; } = string.Empty;
|
||||
public string ISBN { get; set; } = string.Empty;
|
||||
|
||||
#region Locks
|
||||
|
||||
public bool AgeRatingLocked { get; set; }
|
||||
public bool TitleNameLocked { get; set; }
|
||||
public bool GenresLocked { get; set; }
|
||||
public bool TagsLocked { get; set; }
|
||||
public bool WriterLocked { get; set; }
|
||||
public bool CharacterLocked { get; set; }
|
||||
public bool ColoristLocked { get; set; }
|
||||
public bool EditorLocked { get; set; }
|
||||
public bool InkerLocked { get; set; }
|
||||
public bool ImprintLocked { get; set; }
|
||||
public bool LettererLocked { get; set; }
|
||||
public bool PencillerLocked { get; set; }
|
||||
public bool PublisherLocked { get; set; }
|
||||
public bool TranslatorLocked { get; set; }
|
||||
public bool TeamLocked { get; set; }
|
||||
public bool LocationLocked { get; set; }
|
||||
public bool CoverArtistLocked { get; set; }
|
||||
public bool LanguageLocked { get; set; }
|
||||
public bool SummaryLocked { get; set; }
|
||||
public bool ISBNLocked { get; set; }
|
||||
public bool ReleaseDateLocked { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// All people attached at a Chapter level. Usually Comics will have different people per issue.
|
||||
/// </summary>
|
||||
|
|
|
@ -32,13 +32,13 @@ public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
|||
/// The maximum number in the Name field (same as Minimum if Name isn't a range)
|
||||
/// </summary>
|
||||
public required float MaxNumber { get; set; }
|
||||
public IList<Chapter> Chapters { get; set; } = null!;
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
|
||||
public string? CoverImage { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
public string PrimaryColor { get; set; }
|
||||
public string SecondaryColor { get; set; }
|
||||
|
||||
|
@ -57,6 +57,7 @@ public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
|||
|
||||
|
||||
// Relationships
|
||||
public IList<Chapter> Chapters { get; set; } = null!;
|
||||
public Series Series { get; set; } = null!;
|
||||
public int SeriesId { get; set; }
|
||||
|
||||
|
|
|
@ -59,6 +59,18 @@ public static class IncludesExtensions
|
|||
.Include(c => c.People);
|
||||
}
|
||||
|
||||
if (includes.HasFlag(ChapterIncludes.Genres))
|
||||
{
|
||||
queryable = queryable
|
||||
.Include(c => c.Genres);
|
||||
}
|
||||
|
||||
if (includes.HasFlag(ChapterIncludes.Tags))
|
||||
{
|
||||
queryable = queryable
|
||||
.Include(c => c.Tags);
|
||||
}
|
||||
|
||||
return queryable.AsSplitQuery();
|
||||
}
|
||||
|
||||
|
|
|
@ -108,4 +108,49 @@ public static class GenreHelper
|
|||
onModified();
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateGenreList(ICollection<GenreTagDto>? tags, Chapter chapter,
|
||||
IReadOnlyCollection<Genre> allTags, Action<Genre> handleAdd, Action onModified)
|
||||
{
|
||||
// TODO: Write some unit tests
|
||||
if (tags == null) return;
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = chapter.Genres.ToList();
|
||||
foreach (var existing in existingTags)
|
||||
{
|
||||
if (tags.SingleOrDefault(t => t.Title.ToNormalized().Equals(existing.NormalizedTitle)) == null)
|
||||
{
|
||||
// Remove tag
|
||||
chapter.Genres.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||
{
|
||||
var normalizedTitle = tagTitle.ToNormalized();
|
||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle));
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (chapter.Genres.All(t => !t.NormalizedTitle.Equals(normalizedTitle)))
|
||||
{
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new GenreBuilder(tagTitle).Build());
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,18 +165,76 @@ public static class PersonHelper
|
|||
}
|
||||
}
|
||||
|
||||
public static bool HasAnyPeople(SeriesMetadataDto? seriesMetadata)
|
||||
public static void UpdatePeopleList(PersonRole role, ICollection<PersonDto>? people, Chapter chapter, IReadOnlyCollection<Person> allPeople,
|
||||
Action<Person> handleAdd, Action onModified)
|
||||
{
|
||||
if (seriesMetadata == null) return false;
|
||||
return seriesMetadata.Writers.Any() ||
|
||||
seriesMetadata.CoverArtists.Any() ||
|
||||
seriesMetadata.Publishers.Any() ||
|
||||
seriesMetadata.Characters.Any() ||
|
||||
seriesMetadata.Pencillers.Any() ||
|
||||
seriesMetadata.Inkers.Any() ||
|
||||
seriesMetadata.Colorists.Any() ||
|
||||
seriesMetadata.Letterers.Any() ||
|
||||
seriesMetadata.Editors.Any() ||
|
||||
seriesMetadata.Translators.Any();
|
||||
if (people == null) return;
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = chapter.People.Where(p => p.Role == role).ToList();
|
||||
foreach (var existing in existingTags)
|
||||
{
|
||||
if (people.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role
|
||||
{
|
||||
// Remove tag
|
||||
chapter.People.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tag in people)
|
||||
{
|
||||
var existingTag = allPeople.FirstOrDefault(t => t.Name == tag.Name && t.Role == tag.Role);
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (chapter.People.Where(t => t.Role == tag.Role).All(t => t.Name != null && !t.Name.Equals(tag.Name)))
|
||||
{
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new PersonBuilder(tag.Name, role).Build());
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAnyPeople(SeriesMetadataDto? dto)
|
||||
{
|
||||
if (dto == null) return false;
|
||||
return dto.Writers.Count != 0 ||
|
||||
dto.CoverArtists.Count != 0 ||
|
||||
dto.Publishers.Count != 0 ||
|
||||
dto.Characters.Count != 0 ||
|
||||
dto.Pencillers.Count != 0 ||
|
||||
dto.Inkers.Count != 0 ||
|
||||
dto.Colorists.Count != 0 ||
|
||||
dto.Letterers.Count != 0 ||
|
||||
dto.Editors.Count != 0 ||
|
||||
dto.Translators.Count != 0;
|
||||
}
|
||||
|
||||
public static bool HasAnyPeople(UpdateChapterDto? dto)
|
||||
{
|
||||
if (dto == null) return false;
|
||||
return dto.Writers.Count != 0 ||
|
||||
dto.CoverArtists.Count != 0 ||
|
||||
dto.Publishers.Count != 0 ||
|
||||
dto.Characters.Count != 0 ||
|
||||
dto.Pencillers.Count != 0 ||
|
||||
dto.Inkers.Count != 0 ||
|
||||
dto.Colorists.Count != 0 ||
|
||||
dto.Letterers.Count != 0 ||
|
||||
dto.Editors.Count != 0 ||
|
||||
dto.Translators.Count != 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,6 +151,48 @@ public static class TagHelper
|
|||
onModified();
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateTagList(ICollection<TagDto>? tags, Chapter chapter, IReadOnlyCollection<Tag> allTags, Action<Tag> handleAdd, Action onModified)
|
||||
{
|
||||
if (tags == null) return;
|
||||
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = chapter.Tags.ToList();
|
||||
foreach (var existing in existingTags.Where(existing => tags.SingleOrDefault(t => t.Id == existing.Id) == null))
|
||||
{
|
||||
// Remove tag
|
||||
chapter.Tags.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||
{
|
||||
var normalizedTitle = tagTitle.ToNormalized();
|
||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle));
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (chapter.Tags.All(t => t.NormalizedTitle != normalizedTitle))
|
||||
{
|
||||
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(new TagBuilder(tagTitle).Build());
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
|
|
|
@ -580,8 +580,8 @@ public class ImageService : IImageService
|
|||
{
|
||||
return colors.OrderByDescending(c =>
|
||||
{
|
||||
float max = Math.Max(c.X, Math.Max(c.Y, c.Z));
|
||||
float min = Math.Min(c.X, Math.Min(c.Y, c.Z));
|
||||
var max = Math.Max(c.X, Math.Max(c.Y, c.Z));
|
||||
var min = Math.Min(c.X, Math.Min(c.Y, c.Z));
|
||||
return (max - min) / max;
|
||||
}).ToList();
|
||||
}
|
||||
|
|
|
@ -75,8 +75,10 @@ public class MetadataService : IMetadataService
|
|||
/// <param name="chapter"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
/// <param name="encodeFormat">Convert image to Encoding Format when extracting the cover</param>
|
||||
private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize)
|
||||
private Task<bool> UpdateChapterCoverImage(Chapter? chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize)
|
||||
{
|
||||
if (chapter == null) return Task.FromResult(false);
|
||||
|
||||
var firstFile = chapter.Files.MinBy(x => x.Chapter);
|
||||
if (firstFile == null) return Task.FromResult(false);
|
||||
|
||||
|
@ -133,7 +135,9 @@ public class MetadataService : IMetadataService
|
|||
private Task<bool> UpdateVolumeCoverImage(Volume? volume, bool forceUpdate)
|
||||
{
|
||||
// We need to check if Volume coverImage matches first chapters if forceUpdate is false
|
||||
if (volume == null || !_cacheHelper.ShouldUpdateCoverImage(
|
||||
if (volume == null) return Task.FromResult(false);
|
||||
|
||||
if (!_cacheHelper.ShouldUpdateCoverImage(
|
||||
_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage),
|
||||
null, volume.Created, forceUpdate))
|
||||
{
|
||||
|
@ -146,18 +150,20 @@ public class MetadataService : IMetadataService
|
|||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// For cover selection, chapters need to try for issue 1 first, then fallback to first sort order
|
||||
volume.Chapters ??= new List<Chapter>();
|
||||
|
||||
var firstChapter = volume.Chapters.FirstOrDefault(x => x.MinNumber.Is(1f));
|
||||
if (firstChapter == null)
|
||||
if (!volume.CoverImageLocked)
|
||||
{
|
||||
firstChapter = volume.Chapters.MinBy(x => x.SortOrder, ChapterSortComparerDefaultFirst.Default);
|
||||
if (firstChapter == null) return Task.FromResult(false);
|
||||
// For cover selection, chapters need to try for issue 1 first, then fallback to first sort order
|
||||
volume.Chapters ??= new List<Chapter>();
|
||||
|
||||
var firstChapter = volume.Chapters.FirstOrDefault(x => x.MinNumber.Is(1f));
|
||||
if (firstChapter == null)
|
||||
{
|
||||
firstChapter = volume.Chapters.MinBy(x => x.SortOrder, ChapterSortComparerDefaultFirst.Default);
|
||||
if (firstChapter == null) return Task.FromResult(false);
|
||||
}
|
||||
|
||||
volume.CoverImage = firstChapter.CoverImage;
|
||||
}
|
||||
|
||||
|
||||
volume.CoverImage = firstChapter.CoverImage;
|
||||
_imageService.UpdateColorScape(volume);
|
||||
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume));
|
||||
|
|
|
@ -129,62 +129,81 @@ public class ParseScannedFiles
|
|||
|
||||
var result = new List<ScanResult>();
|
||||
|
||||
// Not to self: this whole thing can be parallelized because we don't deal with any DB or global state
|
||||
if (scanDirectoryByDirectory)
|
||||
{
|
||||
var directories = _directoryService.GetDirectories(folderPath, matcher).Select(Parser.Parser.NormalizePath);
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.FileScanProgressEvent(directory, library.Name, ProgressEventType.Updated));
|
||||
|
||||
|
||||
if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, directory, forceCheck))
|
||||
{
|
||||
if (result.Exists(r => r.Folder == directory))
|
||||
{
|
||||
_logger.LogDebug("[ProcessFiles] Skipping adding {Directory} as it's already added", directory);
|
||||
continue;
|
||||
}
|
||||
result.Add(CreateScanResult(directory, folderPath, false, ArraySegment<string>.Empty));
|
||||
}
|
||||
else if (!forceCheck && seriesPaths.TryGetValue(directory, out var series) && series.Count > 1 && series.All(s => !string.IsNullOrEmpty(s.LowestFolderPath)))
|
||||
{
|
||||
// If there are multiple series inside this path, let's check each of them to see which was modified and only scan those
|
||||
// This is very helpful for ComicVine libraries by Publisher
|
||||
_logger.LogDebug("[ProcessFiles] {Directory} is dirty and has multiple series folders, checking if we can avoid a full scan", directory);
|
||||
foreach (var seriesModified in series)
|
||||
{
|
||||
var hasFolderChangedSinceLastScan = seriesModified.LastScanned.Truncate(TimeSpan.TicksPerSecond) <
|
||||
_directoryService
|
||||
.GetLastWriteTime(seriesModified.LowestFolderPath!)
|
||||
.Truncate(TimeSpan.TicksPerSecond);
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.FileScanProgressEvent(seriesModified.LowestFolderPath!, library.Name, ProgressEventType.Updated));
|
||||
|
||||
if (!hasFolderChangedSinceLastScan)
|
||||
{
|
||||
_logger.LogTrace("[ProcessFiles] {Directory} subfolder {Folder} did not change since last scan, adding entry to skip", directory, seriesModified.LowestFolderPath);
|
||||
result.Add(CreateScanResult(seriesModified.LowestFolderPath!, folderPath, false, ArraySegment<string>.Empty));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogTrace("[ProcessFiles] {Directory} subfolder {Folder} changed for Series {SeriesName}", directory, seriesModified.LowestFolderPath, seriesModified.SeriesName);
|
||||
result.Add(CreateScanResult(directory, folderPath, true,
|
||||
_directoryService.ScanFiles(seriesModified.LowestFolderPath!, fileExtensions, matcher)));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(CreateScanResult(directory, folderPath, true,
|
||||
_directoryService.ScanFiles(directory, fileExtensions, matcher)));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return await ScanDirectories(folderPath, seriesPaths, library, forceCheck, matcher, result, fileExtensions);
|
||||
}
|
||||
|
||||
return await ScanSingleDirectory(folderPath, seriesPaths, library, forceCheck, result, fileExtensions, matcher);
|
||||
}
|
||||
|
||||
private async Task<IList<ScanResult>> ScanDirectories(string folderPath, IDictionary<string, IList<SeriesModified>> seriesPaths, Library library, bool forceCheck,
|
||||
GlobMatcher matcher, List<ScanResult> result, string fileExtensions)
|
||||
{
|
||||
var directories = _directoryService.GetDirectories(folderPath, matcher).Select(Parser.Parser.NormalizePath);
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.FileScanProgressEvent(directory, library.Name, ProgressEventType.Updated));
|
||||
|
||||
|
||||
if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, directory, forceCheck))
|
||||
{
|
||||
if (result.Exists(r => r.Folder == directory))
|
||||
{
|
||||
_logger.LogDebug("[ProcessFiles] Skipping adding {Directory} as it's already added", directory);
|
||||
continue;
|
||||
}
|
||||
_logger.LogDebug("[ProcessFiles] Skipping {Directory} as it hasn't changed since last scan", directory);
|
||||
result.Add(CreateScanResult(directory, folderPath, false, ArraySegment<string>.Empty));
|
||||
}
|
||||
else if (!forceCheck && seriesPaths.TryGetValue(directory, out var series)
|
||||
&& series.Count > 1 && series.All(s => !string.IsNullOrEmpty(s.LowestFolderPath)))
|
||||
{
|
||||
// If there are multiple series inside this path, let's check each of them to see which was modified and only scan those
|
||||
// This is very helpful for ComicVine libraries by Publisher
|
||||
|
||||
// TODO: BUG: We might miss new folders this way. Likely need to get all folder names and see if there are any that aren't in known series list
|
||||
|
||||
_logger.LogDebug("[ProcessFiles] {Directory} is dirty and has multiple series folders, checking if we can avoid a full scan", directory);
|
||||
foreach (var seriesModified in series)
|
||||
{
|
||||
var hasFolderChangedSinceLastScan = seriesModified.LastScanned.Truncate(TimeSpan.TicksPerSecond) <
|
||||
_directoryService
|
||||
.GetLastWriteTime(seriesModified.LowestFolderPath!)
|
||||
.Truncate(TimeSpan.TicksPerSecond);
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.FileScanProgressEvent(seriesModified.LowestFolderPath!, library.Name, ProgressEventType.Updated));
|
||||
|
||||
if (!hasFolderChangedSinceLastScan)
|
||||
{
|
||||
_logger.LogDebug("[ProcessFiles] {Directory} subfolder {Folder} did not change since last scan, adding entry to skip", directory, seriesModified.LowestFolderPath);
|
||||
result.Add(CreateScanResult(seriesModified.LowestFolderPath!, folderPath, false, ArraySegment<string>.Empty));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("[ProcessFiles] {Directory} subfolder {Folder} changed for Series {SeriesName}", directory, seriesModified.LowestFolderPath, seriesModified.SeriesName);
|
||||
result.Add(CreateScanResult(directory, folderPath, true,
|
||||
_directoryService.ScanFiles(seriesModified.LowestFolderPath!, fileExtensions, matcher)));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("[ProcessFiles] Performing file scan on {Directory}", directory);
|
||||
var files = _directoryService.ScanFiles(directory, fileExtensions, matcher);
|
||||
result.Add(CreateScanResult(directory, folderPath, true, files));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<IList<ScanResult>> ScanSingleDirectory(string folderPath, IDictionary<string, IList<SeriesModified>> seriesPaths, Library library, bool forceCheck, List<ScanResult> result,
|
||||
string fileExtensions, GlobMatcher matcher)
|
||||
{
|
||||
var normalizedPath = Parser.Parser.NormalizePath(folderPath);
|
||||
var libraryRoot =
|
||||
library.Folders.FirstOrDefault(f =>
|
||||
|
@ -204,7 +223,6 @@ public class ParseScannedFiles
|
|||
_directoryService.ScanFiles(folderPath, fileExtensions, matcher)));
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -760,12 +760,14 @@ public class ProcessSeries : IProcessSeries
|
|||
chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture);
|
||||
chapter.MinNumber = Parser.Parser.MinNumberFromRange(info.Chapters);
|
||||
chapter.MaxNumber = Parser.Parser.MaxNumberFromRange(info.Chapters);
|
||||
chapter.Range = chapter.GetNumberTitle();
|
||||
|
||||
if (!chapter.SortOrderLocked)
|
||||
{
|
||||
chapter.SortOrder = info.IssueOrder;
|
||||
}
|
||||
chapter.Range = chapter.GetNumberTitle();
|
||||
if (float.TryParse(chapter.Title, out var _))
|
||||
|
||||
if (float.TryParse(chapter.Title, out _))
|
||||
{
|
||||
// If we have float based chapters, first scan can have the chapter formatted as Chapter 0.2 - .2 as the title is wrong.
|
||||
chapter.Title = chapter.GetNumberTitle();
|
||||
|
@ -832,19 +834,22 @@ public class ProcessSeries : IProcessSeries
|
|||
|
||||
_logger.LogTrace("[ScannerService] Read ComicInfo for {File}", firstFile.FilePath);
|
||||
|
||||
chapter.AgeRating = ComicInfo.ConvertAgeRatingToEnum(comicInfo.AgeRating);
|
||||
if (!chapter.AgeRatingLocked)
|
||||
{
|
||||
chapter.AgeRating = ComicInfo.ConvertAgeRatingToEnum(comicInfo.AgeRating);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(comicInfo.Title))
|
||||
if (!chapter.TitleNameLocked && !string.IsNullOrEmpty(comicInfo.Title))
|
||||
{
|
||||
chapter.TitleName = comicInfo.Title.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(comicInfo.Summary))
|
||||
if (!chapter.SummaryLocked && !string.IsNullOrEmpty(comicInfo.Summary))
|
||||
{
|
||||
chapter.Summary = comicInfo.Summary;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(comicInfo.LanguageISO))
|
||||
if (!chapter.LanguageLocked && !string.IsNullOrEmpty(comicInfo.LanguageISO))
|
||||
{
|
||||
chapter.Language = comicInfo.LanguageISO;
|
||||
}
|
||||
|
@ -888,7 +893,7 @@ public class ProcessSeries : IProcessSeries
|
|||
// For each weblink, try to parse out some MetadataIds and store in the Chapter directly for matching (CBL)
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(comicInfo.Isbn))
|
||||
if (!chapter.ISBNLocked && !string.IsNullOrEmpty(comicInfo.Isbn))
|
||||
{
|
||||
chapter.ISBN = comicInfo.Isbn;
|
||||
}
|
||||
|
@ -902,84 +907,129 @@ public class ProcessSeries : IProcessSeries
|
|||
chapter.Count = comicInfo.CalculatedCount();
|
||||
|
||||
|
||||
if (comicInfo.Year > 0)
|
||||
if (!chapter.ReleaseDateLocked && comicInfo.Year > 0)
|
||||
{
|
||||
var day = Math.Max(comicInfo.Day, 1);
|
||||
var month = Math.Max(comicInfo.Month, 1);
|
||||
chapter.ReleaseDate = new DateTime(comicInfo.Year, month, day);
|
||||
}
|
||||
|
||||
var people = TagHelper.GetTagValues(comicInfo.Colorist);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Colorist);
|
||||
await UpdatePeople(chapter, people, PersonRole.Colorist);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Characters);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Character);
|
||||
await UpdatePeople(chapter, people, PersonRole.Character);
|
||||
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Translator);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Translator);
|
||||
await UpdatePeople(chapter, people, PersonRole.Translator);
|
||||
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Writer);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Writer);
|
||||
await UpdatePeople(chapter, people, PersonRole.Writer);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Editor);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Editor);
|
||||
await UpdatePeople(chapter, people, PersonRole.Editor);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Inker);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Inker);
|
||||
await UpdatePeople(chapter, people, PersonRole.Inker);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Letterer);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Letterer);
|
||||
await UpdatePeople(chapter, people, PersonRole.Letterer);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Penciller);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Penciller);
|
||||
await UpdatePeople(chapter, people, PersonRole.Penciller);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.CoverArtist);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.CoverArtist);
|
||||
await UpdatePeople(chapter, people, PersonRole.CoverArtist);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Publisher);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Publisher);
|
||||
await UpdatePeople(chapter, people, PersonRole.Publisher);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Imprint);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Imprint);
|
||||
await UpdatePeople(chapter, people, PersonRole.Imprint);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Teams);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Team);
|
||||
await UpdatePeople(chapter, people, PersonRole.Team);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Locations);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Location);
|
||||
await UpdatePeople(chapter, people, PersonRole.Location);
|
||||
|
||||
var genres = TagHelper.GetTagValues(comicInfo.Genre);
|
||||
GenreHelper.KeepOnlySameGenreBetweenLists(chapter.Genres,
|
||||
genres.Select(g => new GenreBuilder(g).Build()).ToList());
|
||||
foreach (var genre in genres)
|
||||
if (!chapter.ColoristLocked)
|
||||
{
|
||||
var g = await _tagManagerService.GetGenre(genre);
|
||||
if (g == null) continue;
|
||||
chapter.Genres.Add(g);
|
||||
var people = TagHelper.GetTagValues(comicInfo.Colorist);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Colorist);
|
||||
await UpdatePeople(chapter, people, PersonRole.Colorist);
|
||||
}
|
||||
|
||||
var tags = TagHelper.GetTagValues(comicInfo.Tags);
|
||||
TagHelper.KeepOnlySameTagBetweenLists(chapter.Tags, tags.Select(t => new TagBuilder(t).Build()).ToList());
|
||||
foreach (var tag in tags)
|
||||
if (!chapter.CharacterLocked)
|
||||
{
|
||||
var t = await _tagManagerService.GetTag(tag);
|
||||
if (t == null) continue;
|
||||
chapter.Tags.Add(t);
|
||||
var people = TagHelper.GetTagValues(comicInfo.Characters);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Character);
|
||||
await UpdatePeople(chapter, people, PersonRole.Character);
|
||||
}
|
||||
|
||||
|
||||
if (!chapter.TranslatorLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Translator);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Translator);
|
||||
await UpdatePeople(chapter, people, PersonRole.Translator);
|
||||
}
|
||||
|
||||
if (!chapter.WriterLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Writer);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Writer);
|
||||
await UpdatePeople(chapter, people, PersonRole.Writer);
|
||||
}
|
||||
|
||||
if (!chapter.EditorLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Editor);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Editor);
|
||||
await UpdatePeople(chapter, people, PersonRole.Editor);
|
||||
}
|
||||
|
||||
if (!chapter.InkerLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Inker);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Inker);
|
||||
await UpdatePeople(chapter, people, PersonRole.Inker);
|
||||
}
|
||||
|
||||
if (!chapter.LettererLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Letterer);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Letterer);
|
||||
await UpdatePeople(chapter, people, PersonRole.Letterer);
|
||||
}
|
||||
|
||||
if (!chapter.PencillerLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Penciller);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Penciller);
|
||||
await UpdatePeople(chapter, people, PersonRole.Penciller);
|
||||
}
|
||||
|
||||
if (!chapter.CoverArtistLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.CoverArtist);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.CoverArtist);
|
||||
await UpdatePeople(chapter, people, PersonRole.CoverArtist);
|
||||
}
|
||||
|
||||
if (!chapter.PublisherLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Publisher);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Publisher);
|
||||
await UpdatePeople(chapter, people, PersonRole.Publisher);
|
||||
}
|
||||
|
||||
if (!chapter.ImprintLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Imprint);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Imprint);
|
||||
await UpdatePeople(chapter, people, PersonRole.Imprint);
|
||||
}
|
||||
|
||||
if (!chapter.TeamLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Teams);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Team);
|
||||
await UpdatePeople(chapter, people, PersonRole.Team);
|
||||
}
|
||||
|
||||
if (!chapter.LocationLocked)
|
||||
{
|
||||
var people = TagHelper.GetTagValues(comicInfo.Locations);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Location);
|
||||
await UpdatePeople(chapter, people, PersonRole.Location);
|
||||
}
|
||||
|
||||
|
||||
if (!chapter.GenresLocked)
|
||||
{
|
||||
var genres = TagHelper.GetTagValues(comicInfo.Genre);
|
||||
GenreHelper.KeepOnlySameGenreBetweenLists(chapter.Genres,
|
||||
genres.Select(g => new GenreBuilder(g).Build()).ToList());
|
||||
foreach (var genre in genres)
|
||||
{
|
||||
var g = await _tagManagerService.GetGenre(genre);
|
||||
if (g == null) continue;
|
||||
chapter.Genres.Add(g);
|
||||
}
|
||||
}
|
||||
|
||||
if (!chapter.TagsLocked)
|
||||
{
|
||||
var tags = TagHelper.GetTagValues(comicInfo.Tags);
|
||||
TagHelper.KeepOnlySameTagBetweenLists(chapter.Tags, tags.Select(t => new TagBuilder(t).Build()).ToList());
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
var t = await _tagManagerService.GetTag(tag);
|
||||
if (t == null) continue;
|
||||
chapter.Tags.Add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -138,6 +138,14 @@ public static class MessageFactory
|
|||
/// A Progress event when a smart collection is synchronizing
|
||||
/// </summary>
|
||||
public const string SmartCollectionSync = "SmartCollectionSync";
|
||||
/// <summary>
|
||||
/// Chapter is removed from server
|
||||
/// </summary>
|
||||
public const string ChapterRemoved = "ChapterRemoved";
|
||||
/// <summary>
|
||||
/// Volume is removed from server
|
||||
/// </summary>
|
||||
public const string VolumeRemoved = "VolumeRemoved";
|
||||
|
||||
public static SignalRMessage DashboardUpdateEvent(int userId)
|
||||
{
|
||||
|
@ -213,6 +221,32 @@ public static class MessageFactory
|
|||
};
|
||||
}
|
||||
|
||||
public static SignalRMessage ChapterRemovedEvent(int chapterId, int seriesId)
|
||||
{
|
||||
return new SignalRMessage()
|
||||
{
|
||||
Name = ChapterRemoved,
|
||||
Body = new
|
||||
{
|
||||
SeriesId = seriesId,
|
||||
ChapterId = chapterId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static SignalRMessage VolumeRemovedEvent(int volumeId, int seriesId)
|
||||
{
|
||||
return new SignalRMessage()
|
||||
{
|
||||
Name = VolumeRemoved,
|
||||
Body = new
|
||||
{
|
||||
SeriesId = seriesId,
|
||||
VolumeId = volumeId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static SignalRMessage WordCountAnalyzerProgressEvent(int libraryId, float progress, string eventType, string subtitle = "")
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue