diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs
index 4872342ef..475cafc71 100644
--- a/API/Controllers/ChapterController.cs
+++ b/API/Controllers/ChapterController.cs
@@ -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;
}
+ ///
+ /// Gets a single chapter
+ ///
+ ///
+ ///
[HttpGet]
public async Task> GetChapter(int chapterId)
{
@@ -25,5 +45,234 @@ public class ChapterController : BaseApiController
return Ok(chapter);
}
+ ///
+ /// Removes a Chapter
+ ///
+ ///
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ [HttpDelete]
+ public async Task> 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);
+ }
+
+ ///
+ /// Update chapter metadata
+ ///
+ ///
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ [HttpPost("update")]
+ public async Task 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();
+ 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();
+ 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();
+ 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();
+ }
+
+
}
diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs
index db0b134ef..6c23f5652 100644
--- a/API/Controllers/ReadingListController.cs
+++ b/API/Controllers/ReadingListController.cs
@@ -63,7 +63,7 @@ public class ReadingListController : BaseApiController
}
///
- /// 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.
///
///
///
@@ -74,6 +74,18 @@ public class ReadingListController : BaseApiController
seriesId, true));
}
+ ///
+ /// Returns all Reading Lists the user has access to that has the given chapter within it.
+ ///
+ ///
+ ///
+ [HttpGet("lists-for-chapter")]
+ public async Task>> GetListsForChapter(int chapterId)
+ {
+ return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForChapterAndUserAsync(User.GetUserId(),
+ chapterId, true));
+ }
+
///
/// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress
///
diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs
index 03841f26d..bfa958546 100644
--- a/API/Controllers/UploadController.cs
+++ b/API/Controllers/UploadController.cs
@@ -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 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
///
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset-chapter-lock")]
+ [Obsolete("Use LockCover in UploadFileDto")]
public async Task ResetChapterLock(UploadFileDto uploadFileDto)
{
try
@@ -461,4 +488,6 @@ public class UploadController : BaseApiController
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock"));
}
+
+
}
diff --git a/API/Controllers/VolumeController.cs b/API/Controllers/VolumeController.cs
new file mode 100644
index 000000000..5d23336b4
--- /dev/null
+++ b/API/Controllers/VolumeController.cs
@@ -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> GetVolume(int volumeId)
+ {
+ var volume =
+ await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, User.GetUserId());
+
+ return Ok(volume);
+ }
+
+ [Authorize(Policy = "RequireAdminRole")]
+ [HttpDelete]
+ public async Task> 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);
+ }
+}
diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs
index a42c418af..067e64ad4 100644
--- a/API/DTOs/ChapterDto.cs
+++ b/API/DTOs/ChapterDto.cs
@@ -158,6 +158,33 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage
///
public int TotalCount { get; set; }
+ public bool LanguageLocked { get; set; }
+ public bool SummaryLocked { get; set; }
+ ///
+ /// Locked by user so metadata updates from scan loop will not override AgeRating
+ ///
+ public bool AgeRatingLocked { get; set; }
+ ///
+ /// Locked by user so metadata updates from scan loop will not override PublicationStatus
+ ///
+ 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; }
diff --git a/API/DTOs/Metadata/ChapterMetadataDto.cs b/API/DTOs/Metadata/ChapterMetadataDto.cs
index c5c56d15f..bbd93d618 100644
--- a/API/DTOs/Metadata/ChapterMetadataDto.cs
+++ b/API/DTOs/Metadata/ChapterMetadataDto.cs
@@ -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;
///
/// Exclusively metadata about a given chapter
///
+[Obsolete("Will not be maintained as of v0.8.1")]
public class ChapterMetadataDto
{
public int Id { get; set; }
diff --git a/API/DTOs/UpdateChapterDto.cs b/API/DTOs/UpdateChapterDto.cs
new file mode 100644
index 000000000..2ca0a12a9
--- /dev/null
+++ b/API/DTOs/UpdateChapterDto.cs
@@ -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;
+
+ ///
+ /// Genres for the Chapter
+ ///
+ public ICollection Genres { get; set; } = new List();
+ ///
+ /// Collection of all Tags from underlying chapters for a Chapter
+ ///
+ public ICollection Tags { get; set; } = new List();
+
+ public ICollection Writers { get; set; } = new List();
+ public ICollection CoverArtists { get; set; } = new List();
+ public ICollection Publishers { get; set; } = new List();
+ public ICollection Characters { get; set; } = new List();
+ public ICollection Pencillers { get; set; } = new List();
+ public ICollection Inkers { get; set; } = new List();
+ public ICollection Imprints { get; set; } = new List();
+ public ICollection Colorists { get; set; } = new List();
+ public ICollection Letterers { get; set; } = new List();
+ public ICollection Editors { get; set; } = new List();
+ public ICollection Translators { get; set; } = new List();
+ public ICollection Teams { get; set; } = new List();
+ public ICollection Locations { get; set; } = new List();
+
+ ///
+ /// Highest Age Rating from all Chapters
+ ///
+ public AgeRating AgeRating { get; set; } = AgeRating.Unknown;
+ ///
+ /// Language of the content (BCP-47 code)
+ ///
+ public string Language { get; set; } = string.Empty;
+
+
+ ///
+ /// Locked by user so metadata updates from scan loop will not override AgeRating
+ ///
+ 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; }
+
+ ///
+ /// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.
+ ///
+ public float SortOrder { get; set; }
+ ///
+ /// Can the sort order be updated on scan or is it locked from UI
+ ///
+ public bool SortOrderLocked { get; set; }
+
+ ///
+ /// Comma-separated link of urls to external services that have some relation to the Chapter
+ ///
+ public string WebLinks { get; set; } = string.Empty;
+ public string ISBN { get; set; } = string.Empty;
+ ///
+ /// Date which chapter was released
+ ///
+ public DateTime ReleaseDate { get; set; }
+ ///
+ /// Chapter title
+ ///
+ /// This should not be confused with Title which is used for special filenames.
+ public string TitleName { get; set; } = string.Empty;
+}
diff --git a/API/DTOs/Uploads/UploadFileDto.cs b/API/DTOs/Uploads/UploadFileDto.cs
index 236a554b8..72fe7da9b 100644
--- a/API/DTOs/Uploads/UploadFileDto.cs
+++ b/API/DTOs/Uploads/UploadFileDto.cs
@@ -10,4 +10,9 @@ public class UploadFileDto
/// Base Url encoding of the file to upload from (can be null)
///
public required string Url { get; set; }
+
+ ///
+ /// Lock the cover or not
+ ///
+ public bool LockCover { get; set; } = true;
}
diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs
index bceccd43a..fd3202481 100644
--- a/API/DTOs/VolumeDto.cs
+++ b/API/DTOs/VolumeDto.cs
@@ -44,6 +44,7 @@ public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage
public int MaxHoursToRead { get; set; }
///
public int AvgHoursToRead { get; set; }
+ public long WordCount { get; set; }
///
/// 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; }
diff --git a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs b/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs
new file mode 100644
index 000000000..07723e833
--- /dev/null
+++ b/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs
@@ -0,0 +1,3142 @@
+//
+using System;
+using API.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace API.Data.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20240811154857_ChapterMetadataLocks")]
+ partial class ChapterMetadataLocks
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
+
+ modelBuilder.Entity("API.Entities.AppRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRestriction")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRestrictionIncludeUnknowns")
+ .HasColumnType("INTEGER");
+
+ b.Property("AniListAccessToken")
+ .HasColumnType("TEXT");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("ConfirmationToken")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActive")
+ .HasColumnType("TEXT");
+
+ b.Property("LastActiveUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("MalAccessToken")
+ .HasColumnType("TEXT");
+
+ b.Property("MalUserName")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Page")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserBookmark");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserCollection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastSyncUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("MissingSeriesFromSource")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("PrimaryColor")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecondaryColor")
+ .HasColumnType("TEXT");
+
+ b.Property("Source")
+ .HasColumnType("INTEGER");
+
+ b.Property("SourceUrl")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalSourceCount")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserCollection");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserDashboardStream", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsProvided")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("SmartFilterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("StreamType")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(4);
+
+ b.Property("Visible")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SmartFilterId");
+
+ b.HasIndex("Visible");
+
+ b.ToTable("AppUserDashboardStream");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserExternalSource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Host")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserExternalSource");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserOnDeckRemoval");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("AutoCloseMenu")
+ .HasColumnType("INTEGER");
+
+ b.Property("BackgroundColor")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("#000000");
+
+ b.Property("BlurUnreadSummaries")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderImmersiveMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderWritingStyle")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("BookThemeName")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("Dark");
+
+ b.Property("CollapseSeriesRelationships")
+ .HasColumnType("INTEGER");
+
+ b.Property("EmulateBook")
+ .HasColumnType("INTEGER");
+
+ b.Property("GlobalPageLayoutMode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("Locale")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("en");
+
+ b.Property("NoTransitions")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfScrollMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfSpreadMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfTheme")
+ .HasColumnType("INTEGER");
+
+ b.Property("PromptForDownloadSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShareReviews")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowScreenHints")
+ .HasColumnType("INTEGER");
+
+ b.Property("SwipeToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThemeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId")
+ .IsUnique();
+
+ b.HasIndex("ThemeId");
+
+ b.ToTable("AppUserPreferences");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserProgress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookScrollId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PagesRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserProgresses");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRating", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("HasBeenRated")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("REAL");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Tagline")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserRating");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserSideNavStream", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ExternalSourceId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsProvided")
+ .HasColumnType("INTEGER");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("SmartFilterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("StreamType")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(5);
+
+ b.Property("Visible")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SmartFilterId");
+
+ b.HasIndex("Visible");
+
+ b.ToTable("AppUserSideNavStream");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserSmartFilter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Filter")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserSmartFilter");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserTableOfContent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookScrollId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserTableOfContent");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserWantToRead", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserWantToRead");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRatingLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("AlternateCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("AlternateNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("AlternateSeries")
+ .HasColumnType("TEXT");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("CharacterLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ColoristLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverArtistLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("EditorLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("GenresLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ISBN")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("");
+
+ b.Property("ISBNLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ImprintLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("InkerLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LanguageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LettererLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("LocationLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxNumber")
+ .HasColumnType("REAL");
+
+ b.Property("MinHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("MinNumber")
+ .HasColumnType("REAL");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("PencillerLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("PrimaryColor")
+ .HasColumnType("TEXT");
+
+ b.Property("PublisherLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDateLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecondaryColor")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesGroup")
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("REAL");
+
+ b.Property("SortOrderLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("StoryArc")
+ .HasColumnType("TEXT");
+
+ b.Property("StoryArcNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("SummaryLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("TagsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("TeamLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property