diff --git a/API.Tests/Parser/ComicParserTests.cs b/API.Tests/Parser/ComicParserTests.cs
index 6e33dd89c..a18ea21c9 100644
--- a/API.Tests/Parser/ComicParserTests.cs
+++ b/API.Tests/Parser/ComicParserTests.cs
@@ -22,11 +22,12 @@ namespace API.Tests.Parser
[InlineData("Invincible Vol 01 Family matters (2005) (Digital).cbr", "Invincible")]
[InlineData("Amazing Man Comics chapter 25", "Amazing Man Comics")]
[InlineData("Amazing Man Comics issue #25", "Amazing Man Comics")]
+ [InlineData("Teen Titans v1 038 (1972) (c2c).cbr", "Teen Titans")]
public void ParseComicSeriesTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename));
}
-
+
[Theory]
[InlineData("01 Spider-Man & Wolverine 01.cbr", "1")]
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "4")]
@@ -47,7 +48,7 @@ namespace API.Tests.Parser
{
Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename));
}
-
+
[Theory]
[InlineData("01 Spider-Man & Wolverine 01.cbr", "0")]
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")]
@@ -70,4 +71,4 @@ namespace API.Tests.Parser
Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename));
}
}
-}
\ No newline at end of file
+}
diff --git a/API/API.csproj b/API/API.csproj
index d824795e5..58709db38 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -12,6 +12,10 @@
../favicon.ico
+
+ bin\Debug\API.xml
+
+
Kavita
@@ -81,6 +85,8 @@
+
+
diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs
index e09f8592a..6f17e5ada 100644
--- a/API/Controllers/CollectionController.cs
+++ b/API/Controllers/CollectionController.cs
@@ -13,17 +13,25 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
+ ///
+ /// APIs for Collections
+ ///
public class CollectionController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly UserManager _userManager;
+ ///
public CollectionController(IUnitOfWork unitOfWork, UserManager userManager)
{
_unitOfWork = unitOfWork;
_userManager = userManager;
}
+ ///
+ /// Return a list of all collection tags on the server
+ ///
+ ///
[HttpGet]
public async Task> GetAllTags()
{
@@ -31,11 +39,17 @@ namespace API.Controllers
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
if (isAdmin)
{
- return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
+ return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
}
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
}
-
+
+ ///
+ /// Searches against the collection tags on the DB and returns matches that meet the search criteria.
+ /// Search strings will be cleaned of certain fields, like %
+ ///
+ /// Search term
+ ///
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("search")]
public async Task> SearchTags(string queryString)
@@ -43,20 +57,27 @@ namespace API.Controllers
queryString ??= "";
queryString = queryString.Replace(@"%", "");
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
-
+
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
}
-
+
+ ///
+ /// Updates an existing tag with a new title, promotion status, and summary.
+ /// UI does not contain controls to update title
+ ///
+ ///
+ ///
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update")]
- public async Task UpdateTag(CollectionTagDto updatedTag)
+ public async Task UpdateTagPromotion(CollectionTagDto updatedTag)
{
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updatedTag.Id);
if (existingTag == null) return BadRequest("This tag does not exist");
existingTag.Promoted = updatedTag.Promoted;
- existingTag.Title = updatedTag.Title;
+ existingTag.Title = updatedTag.Title.Trim();
existingTag.NormalizedTitle = Parser.Parser.Normalize(updatedTag.Title).ToUpper();
+ existingTag.Summary = updatedTag.Summary.Trim();
if (_unitOfWork.HasChanges())
{
@@ -73,6 +94,11 @@ namespace API.Controllers
return BadRequest("Something went wrong, please try again");
}
+ ///
+ /// For a given tag, update the summary if summary has changed and remove a set of series from the tag.
+ ///
+ ///
+ ///
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update-series")]
public async Task UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
@@ -90,6 +116,13 @@ namespace API.Controllers
_unitOfWork.CollectionTagRepository.Update(tag);
}
+ if (!updateSeriesForTagDto.Tag.CoverImageLocked)
+ {
+ tag.CoverImageLocked = false;
+ tag.CoverImage = Array.Empty();
+ _unitOfWork.CollectionTagRepository.Update(tag);
+ }
+
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
{
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
@@ -101,7 +134,9 @@ namespace API.Controllers
_unitOfWork.CollectionTagRepository.Remove(tag);
}
- if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
+ if (!_unitOfWork.HasChanges()) return Ok("No updates");
+
+ if (await _unitOfWork.CommitAsync())
{
return Ok("Tag updated");
}
@@ -110,9 +145,9 @@ namespace API.Controllers
{
await _unitOfWork.RollbackAsync();
}
-
-
+
+
return BadRequest("Something went wrong. Please try again.");
}
}
-}
\ No newline at end of file
+}
diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs
index 234ef2ae6..31da9c54b 100644
--- a/API/Controllers/ImageController.cs
+++ b/API/Controllers/ImageController.cs
@@ -5,57 +5,78 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
+ ///
+ /// Responsible for servicing up images stored in the DB
+ ///
public class ImageController : BaseApiController
{
+ private const string Format = "jpeg";
private readonly IUnitOfWork _unitOfWork;
+ ///
public ImageController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
-
+
+ ///
+ /// Returns cover image for Chapter
+ ///
+ ///
+ ///
[HttpGet("chapter-cover")]
public async Task GetChapterCoverImage(int chapterId)
{
var content = await _unitOfWork.VolumeRepository.GetChapterCoverImageAsync(chapterId);
if (content == null) return BadRequest("No cover image");
- const string format = "jpeg";
Response.AddCacheHeader(content);
- return File(content, "image/" + format, $"chapterId");
+ return File(content, "image/" + Format, $"{chapterId}");
}
+ ///
+ /// Returns cover image for Volume
+ ///
+ ///
+ ///
[HttpGet("volume-cover")]
public async Task GetVolumeCoverImage(int volumeId)
{
var content = await _unitOfWork.SeriesRepository.GetVolumeCoverImageAsync(volumeId);
if (content == null) return BadRequest("No cover image");
- const string format = "jpeg";
Response.AddCacheHeader(content);
- return File(content, "image/" + format, $"volumeId");
+ return File(content, "image/" + Format, $"{volumeId}");
}
-
+
+ ///
+ /// Returns cover image for Series
+ ///
+ /// Id of Series
+ ///
[HttpGet("series-cover")]
public async Task GetSeriesCoverImage(int seriesId)
{
var content = await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId);
if (content == null) return BadRequest("No cover image");
- const string format = "jpeg";
Response.AddCacheHeader(content);
- return File(content, "image/" + format, $"seriesId");
+ return File(content, "image/" + Format, $"{seriesId}");
}
-
+
+ ///
+ /// Returns cover image for Collection Tag
+ ///
+ ///
+ ///
[HttpGet("collection-cover")]
public async Task GetCollectionCoverImage(int collectionTagId)
{
var content = await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId);
if (content == null) return BadRequest("No cover image");
- const string format = "jpeg";
Response.AddCacheHeader(content);
- return File(content, "image/" + format, $"collectionTagId");
+ return File(content, "image/" + Format, $"{collectionTagId}");
}
}
-}
\ No newline at end of file
+}
diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs
index b1d42b6cb..947ce05a0 100644
--- a/API/Controllers/ReaderController.cs
+++ b/API/Controllers/ReaderController.cs
@@ -37,16 +37,24 @@ namespace API.Controllers
var chapter = await _cacheService.Ensure(chapterId);
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
- var (path, _) = await _cacheService.GetCachedPagePath(chapter, page);
- if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
+ try
+ {
+ var (path, _) = await _cacheService.GetCachedPagePath(chapter, page);
+ if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
- var content = await _directoryService.ReadFileAsync(path);
- var format = Path.GetExtension(path).Replace(".", "");
+ var content = await _directoryService.ReadFileAsync(path);
+ var format = Path.GetExtension(path).Replace(".", "");
- // Calculates SHA1 Hash for byte[]
- Response.AddCacheHeader(content);
+ // Calculates SHA1 Hash for byte[]
+ Response.AddCacheHeader(content);
- return File(content, "image/" + format);
+ return File(content, "image/" + format);
+ }
+ catch (Exception)
+ {
+ _cacheService.CleanupChapters(new []{ chapterId });
+ throw;
+ }
}
[HttpGet("chapter-info")]
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
index 9b00f2c46..c41ae1841 100644
--- a/API/Controllers/SeriesController.cs
+++ b/API/Controllers/SeriesController.cs
@@ -9,6 +9,7 @@ using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Interfaces;
+using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -45,11 +46,26 @@ namespace API.Controllers
return Ok(series);
}
+ ///
+ /// Fetches a Series for a given Id
+ ///
+ /// Series Id to fetch details for
+ ///
+ /// Throws an exception if the series Id does exist
[HttpGet("{seriesId}")]
public async Task> GetSeries(int seriesId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
- return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id));
+ try
+ {
+ return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id));
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "There was an issue fetching {SeriesId}", seriesId);
+ throw new KavitaException("This series does not exist");
+ }
+
}
[Authorize(Policy = "RequireAdminRole")]
@@ -138,10 +154,21 @@ namespace API.Controllers
series.SortName = updateSeries.SortName.Trim();
series.Summary = updateSeries.Summary.Trim();
+ var needsRefreshMetadata = false;
+ if (!updateSeries.CoverImageLocked)
+ {
+ series.CoverImageLocked = false;
+ needsRefreshMetadata = true;
+ }
+
_unitOfWork.SeriesRepository.Update(series);
if (await _unitOfWork.CommitAsync())
{
+ if (needsRefreshMetadata)
+ {
+ _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
+ }
return Ok();
}
@@ -274,6 +301,12 @@ namespace API.Controllers
return BadRequest("Could not update metadata");
}
+ ///
+ /// Returns all Series grouped by the passed Collection Id with Pagination.
+ ///
+ /// Collection Id to pull series from
+ /// Pagination information
+ ///
[HttpGet("series-by-collection")]
public async Task>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
{
diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs
new file mode 100644
index 000000000..0d924c66d
--- /dev/null
+++ b/API/Controllers/UploadController.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Threading.Tasks;
+using API.DTOs.Uploads;
+using API.Interfaces;
+using API.Interfaces.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace API.Controllers
+{
+ ///
+ ///
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ public class UploadController : BaseApiController
+ {
+ private readonly IUnitOfWork _unitOfWork;
+ private readonly IImageService _imageService;
+ private readonly ILogger _logger;
+ private readonly ITaskScheduler _taskScheduler;
+
+ ///
+ public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger logger, ITaskScheduler taskScheduler)
+ {
+ _unitOfWork = unitOfWork;
+ _imageService = imageService;
+ _logger = logger;
+ _taskScheduler = taskScheduler;
+ }
+
+ ///
+ /// Replaces series cover image and locks it with a base64 encoded image
+ ///
+ ///
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ [RequestSizeLimit(8_000_000)]
+ [HttpPost("series")]
+ public async Task UploadSeriesCoverImageFromUrl(UploadFileDto uploadFileDto)
+ {
+ // Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
+ // See if we can do this all in memory without touching underlying system
+ if (string.IsNullOrEmpty(uploadFileDto.Url))
+ {
+ return BadRequest("You must pass a url to use");
+ }
+
+ try
+ {
+ var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
+ var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
+
+ if (bytes.Length > 0)
+ {
+ series.CoverImage = bytes;
+ series.CoverImageLocked = true;
+ _unitOfWork.SeriesRepository.Update(series);
+ }
+
+ if (_unitOfWork.HasChanges())
+ {
+ await _unitOfWork.CommitAsync();
+ return Ok();
+ }
+
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "There was an issue uploading cover image for Series {Id}", uploadFileDto.Id);
+ await _unitOfWork.RollbackAsync();
+ }
+
+ return BadRequest("Unable to save cover image to Series");
+ }
+
+ ///
+ /// Replaces collection tag cover image and locks it with a base64 encoded image
+ ///
+ ///
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ [RequestSizeLimit(8_000_000)]
+ [HttpPost("collection")]
+ public async Task UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto)
+ {
+ // Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
+ // See if we can do this all in memory without touching underlying system
+ if (string.IsNullOrEmpty(uploadFileDto.Url))
+ {
+ return BadRequest("You must pass a url to use");
+ }
+
+ try
+ {
+ var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
+ var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
+
+ if (bytes.Length > 0)
+ {
+ tag.CoverImage = bytes;
+ tag.CoverImageLocked = true;
+ _unitOfWork.CollectionTagRepository.Update(tag);
+ }
+
+ if (_unitOfWork.HasChanges())
+ {
+ await _unitOfWork.CommitAsync();
+ return Ok();
+ }
+
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "There was an issue uploading cover image for Collection Tag {Id}", uploadFileDto.Id);
+ await _unitOfWork.RollbackAsync();
+ }
+
+ return BadRequest("Unable to save cover image to Collection Tag");
+ }
+
+ ///
+ /// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
+ ///
+ ///
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ [RequestSizeLimit(8_000_000)]
+ [HttpPost("chapter")]
+ public async Task UploadChapterCoverImageFromUrl(UploadFileDto uploadFileDto)
+ {
+ // Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
+ // See if we can do this all in memory without touching underlying system
+ if (string.IsNullOrEmpty(uploadFileDto.Url))
+ {
+ return BadRequest("You must pass a url to use");
+ }
+
+ try
+ {
+ var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
+
+ if (bytes.Length > 0)
+ {
+ var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(uploadFileDto.Id);
+ chapter.CoverImage = bytes;
+ chapter.CoverImageLocked = true;
+ _unitOfWork.ChapterRepository.Update(chapter);
+ var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
+ volume.CoverImage = chapter.CoverImage;
+ _unitOfWork.VolumeRepository.Update(volume);
+ }
+
+ if (_unitOfWork.HasChanges())
+ {
+ await _unitOfWork.CommitAsync();
+ return Ok();
+ }
+
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "There was an issue uploading cover image for Chapter {Id}", uploadFileDto.Id);
+ await _unitOfWork.RollbackAsync();
+ }
+
+ return BadRequest("Unable to save cover image to Chapter");
+ }
+
+ ///
+ /// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
+ ///
+ /// Does not use Url property
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ [HttpPost("reset-chapter-lock")]
+ public async Task ResetChapterLock(UploadFileDto uploadFileDto)
+ {
+ try
+ {
+ var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(uploadFileDto.Id);
+ chapter.CoverImage = Array.Empty();
+ chapter.CoverImageLocked = false;
+ _unitOfWork.ChapterRepository.Update(chapter);
+ var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
+ volume.CoverImage = chapter.CoverImage;
+ _unitOfWork.VolumeRepository.Update(volume);
+ var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
+
+ if (_unitOfWork.HasChanges())
+ {
+ await _unitOfWork.CommitAsync();
+ _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
+ return Ok();
+ }
+
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id);
+ await _unitOfWork.RollbackAsync();
+ }
+
+ return BadRequest("Unable to resetting cover lock for Chapter");
+ }
+
+ }
+}
diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs
index 4dcabee33..d7a4beb64 100644
--- a/API/DTOs/ChapterDto.cs
+++ b/API/DTOs/ChapterDto.cs
@@ -2,6 +2,10 @@
namespace API.DTOs
{
+ ///
+ /// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
+ /// file (abstracted from type).
+ ///
public class ChapterDto
{
public int Id { get; init; }
@@ -10,7 +14,7 @@ namespace API.DTOs
///
public string Range { get; init; }
///
- /// Smallest number of the Range.
+ /// Smallest number of the Range.
///
public string Number { get; init; }
///
@@ -22,7 +26,7 @@ namespace API.DTOs
///
public bool IsSpecial { get; init; }
///
- /// Used for books/specials to display custom title. For non-specials/books, will be set to
+ /// Used for books/specials to display custom title. For non-specials/books, will be set to
///
public string Title { get; init; }
///
@@ -33,6 +37,13 @@ namespace API.DTOs
/// Calculated at API time. Number of pages read for this Chapter for logged in user.
///
public int PagesRead { get; set; }
+ ///
+ /// If the Cover Image is locked for this entity
+ ///
+ public bool CoverImageLocked { get; set; }
+ ///
+ /// Volume Id this Chapter belongs to
+ ///
public int VolumeId { get; init; }
}
-}
\ No newline at end of file
+}
diff --git a/API/DTOs/CollectionTagDto.cs b/API/DTOs/CollectionTagDto.cs
index 26f256562..cb9870610 100644
--- a/API/DTOs/CollectionTagDto.cs
+++ b/API/DTOs/CollectionTagDto.cs
@@ -6,5 +6,6 @@
public string Title { get; set; }
public string Summary { get; set; }
public bool Promoted { get; set; }
+ public bool CoverImageLocked { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs
index 933bf0408..fc70ce5ed 100644
--- a/API/DTOs/SeriesDto.cs
+++ b/API/DTOs/SeriesDto.cs
@@ -12,6 +12,7 @@ namespace API.DTOs
public string SortName { get; init; }
public string Summary { get; init; }
public int Pages { get; init; }
+ public bool CoverImageLocked { get; set; }
///
/// Sum of pages read from linked Volumes. Calculated at API-time.
///
diff --git a/API/DTOs/UpdateSeriesDto.cs b/API/DTOs/UpdateSeriesDto.cs
index fac3c209e..39054a032 100644
--- a/API/DTOs/UpdateSeriesDto.cs
+++ b/API/DTOs/UpdateSeriesDto.cs
@@ -10,5 +10,6 @@
public byte[] CoverImage { get; init; }
public int UserRating { get; set; }
public string UserReview { get; set; }
+ public bool CoverImageLocked { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/API/DTOs/Uploads/UploadFileDto.cs b/API/DTOs/Uploads/UploadFileDto.cs
new file mode 100644
index 000000000..68a5f7de0
--- /dev/null
+++ b/API/DTOs/Uploads/UploadFileDto.cs
@@ -0,0 +1,14 @@
+namespace API.DTOs.Uploads
+{
+ public class UploadFileDto
+ {
+ ///
+ /// Id of the Entity
+ ///
+ public int Id { get; set; }
+ ///
+ /// Url of the file to download from (can be null)
+ ///
+ public string Url { get; set; }
+ }
+}
diff --git a/API/Data/ChapterRepository.cs b/API/Data/ChapterRepository.cs
new file mode 100644
index 000000000..7e44d8977
--- /dev/null
+++ b/API/Data/ChapterRepository.cs
@@ -0,0 +1,24 @@
+using API.Entities;
+using API.Interfaces.Repositories;
+using AutoMapper;
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Data
+{
+ public class ChapterRepository : IChapterRepository
+ {
+ private readonly DataContext _context;
+
+ public ChapterRepository(DataContext context)
+ {
+ _context = context;
+ }
+
+ public void Update(Chapter chapter)
+ {
+ _context.Entry(chapter).State = EntityState.Modified;
+ }
+
+ // TODO: Move over Chapter based queries here
+ }
+}
diff --git a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs b/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs
new file mode 100644
index 000000000..991c616fc
--- /dev/null
+++ b/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs
@@ -0,0 +1,919 @@
+//
+using System;
+using API.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace API.Data.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20210813010210_CoverImageLockFieldsPart1")]
+ partial class CoverImageLockFieldsPart1
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "5.0.8");
+
+ 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");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActive")
+ .HasColumnType("TEXT");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .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");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ 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.AppUserPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("AutoCloseMenu")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("SiteDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId")
+ .IsUnique();
+
+ 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("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("PagesRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserProgresses");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRating", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("INTEGER");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ 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");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("BLOB");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("VolumeId");
+
+ b.ToTable("Chapter");
+ });
+
+ modelBuilder.Entity("API.Entities.CollectionTag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("BLOB");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id", "Promoted")
+ .IsUnique();
+
+ b.ToTable("CollectionTag");
+ });
+
+ modelBuilder.Entity("API.Entities.FolderPath", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("FolderPath");
+ });
+
+ modelBuilder.Entity("API.Entities.Library", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Library");
+ });
+
+ modelBuilder.Entity("API.Entities.MangaFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("FilePath")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.ToTable("MangaFile");
+ });
+
+ modelBuilder.Entity("API.Entities.Series", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("BLOB");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LocalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalName")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
+ .IsUnique();
+
+ b.ToTable("Series");
+ });
+
+ modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId")
+ .IsUnique();
+
+ b.HasIndex("Id", "SeriesId")
+ .IsUnique();
+
+ b.ToTable("SeriesMetadata");
+ });
+
+ modelBuilder.Entity("API.Entities.ServerSetting", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Key");
+
+ b.ToTable("ServerSetting");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("BLOB");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("INTEGER");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("Volume");
+ });
+
+ modelBuilder.Entity("AppUserLibrary", b =>
+ {
+ b.Property("AppUsersId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LibrariesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("AppUsersId", "LibrariesId");
+
+ b.HasIndex("LibrariesId");
+
+ b.ToTable("AppUserLibrary");
+ });
+
+ modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
+ {
+ b.Property("CollectionTagsId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesMetadatasId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("CollectionTagsId", "SeriesMetadatasId");
+
+ b.HasIndex("SeriesMetadatasId");
+
+ b.ToTable("CollectionTagSeriesMetadata");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithMany("Bookmarks")
+ .HasForeignKey("AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithOne("UserPreferences")
+ .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserProgress", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithMany("Progresses")
+ .HasForeignKey("AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRating", b =>
+ {
+ b.HasOne("API.Entities.AppUser", "AppUser")
+ .WithMany("Ratings")
+ .HasForeignKey("AppUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AppUser");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRole", b =>
+ {
+ b.HasOne("API.Entities.AppRole", "Role")
+ .WithMany("UserRoles")
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.AppUser", "User")
+ .WithMany("UserRoles")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Role");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.HasOne("API.Entities.Volume", "Volume")
+ .WithMany("Chapters")
+ .HasForeignKey("VolumeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Volume");
+ });
+
+ modelBuilder.Entity("API.Entities.FolderPath", b =>
+ {
+ b.HasOne("API.Entities.Library", "Library")
+ .WithMany("Folders")
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Library");
+ });
+
+ modelBuilder.Entity("API.Entities.MangaFile", b =>
+ {
+ b.HasOne("API.Entities.Chapter", "Chapter")
+ .WithMany("Files")
+ .HasForeignKey("ChapterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Chapter");
+ });
+
+ modelBuilder.Entity("API.Entities.Series", b =>
+ {
+ b.HasOne("API.Entities.Library", "Library")
+ .WithMany("Series")
+ .HasForeignKey("LibraryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Library");
+ });
+
+ modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
+ {
+ b.HasOne("API.Entities.Series", "Series")
+ .WithOne("Metadata")
+ .HasForeignKey("API.Entities.SeriesMetadata", "SeriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Series");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.HasOne("API.Entities.Series", "Series")
+ .WithMany("Volumes")
+ .HasForeignKey("SeriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Series");
+ });
+
+ modelBuilder.Entity("AppUserLibrary", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("AppUsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.Library", null)
+ .WithMany()
+ .HasForeignKey("LibrariesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
+ {
+ b.HasOne("API.Entities.CollectionTag", null)
+ .WithMany()
+ .HasForeignKey("CollectionTagsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("API.Entities.SeriesMetadata", null)
+ .WithMany()
+ .HasForeignKey("SeriesMetadatasId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("API.Entities.AppRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("API.Entities.AppRole", b =>
+ {
+ b.Navigation("UserRoles");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Navigation("Bookmarks");
+
+ b.Navigation("Progresses");
+
+ b.Navigation("Ratings");
+
+ b.Navigation("UserPreferences");
+
+ b.Navigation("UserRoles");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Navigation("Files");
+ });
+
+ modelBuilder.Entity("API.Entities.Library", b =>
+ {
+ b.Navigation("Folders");
+
+ b.Navigation("Series");
+ });
+
+ modelBuilder.Entity("API.Entities.Series", b =>
+ {
+ b.Navigation("Metadata");
+
+ b.Navigation("Volumes");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.Navigation("Chapters");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs b/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs
new file mode 100644
index 000000000..1b04826cd
--- /dev/null
+++ b/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs
@@ -0,0 +1,35 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace API.Data.Migrations
+{
+ public partial class CoverImageLockFieldsPart1 : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "CoverImageLocked",
+ table: "Series",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "CoverImageLocked",
+ table: "CollectionTag",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CoverImageLocked",
+ table: "Series");
+
+ migrationBuilder.DropColumn(
+ name: "CoverImageLocked",
+ table: "CollectionTag");
+ }
+ }
+}
diff --git a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs b/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs
new file mode 100644
index 000000000..a7d6f8afe
--- /dev/null
+++ b/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs
@@ -0,0 +1,922 @@
+//
+using System;
+using API.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace API.Data.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20210814215831_CoverImageLockedFieldsPart2")]
+ partial class CoverImageLockedFieldsPart2
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "5.0.8");
+
+ 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");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActive")
+ .HasColumnType("TEXT");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .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");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ 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.AppUserPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("AutoCloseMenu")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("SiteDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId")
+ .IsUnique();
+
+ 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("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("PagesRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserProgresses");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRating", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("INTEGER");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ 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");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("BLOB");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("VolumeId");
+
+ b.ToTable("Chapter");
+ });
+
+ modelBuilder.Entity("API.Entities.CollectionTag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("BLOB");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id", "Promoted")
+ .IsUnique();
+
+ b.ToTable("CollectionTag");
+ });
+
+ modelBuilder.Entity("API.Entities.FolderPath", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property