diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj
index bd78c1a8d..31af4f2c6 100644
--- a/API.Benchmark/API.Benchmark.csproj
+++ b/API.Benchmark/API.Benchmark.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index 5b1cacac2..85350e96d 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -7,10 +7,10 @@
-
+
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/API.Tests/Helpers/SeriesHelperTests.cs b/API.Tests/Helpers/SeriesHelperTests.cs
index 8acd4bb85..a8ffd95c3 100644
--- a/API.Tests/Helpers/SeriesHelperTests.cs
+++ b/API.Tests/Helpers/SeriesHelperTests.cs
@@ -137,6 +137,27 @@ public class SeriesHelperTests
NormalizedName = API.Parser.Parser.Normalize("SomethingRandom")
}));
}
+
+ [Fact]
+ public void FindSeries_ShouldFind_UsingLocalizedName_2()
+ {
+ var series = DbFactory.Series("My Dress-Up Darling");
+ series.LocalizedName = "Sono Bisque Doll wa Koi wo Suru";
+ series.Format = MangaFormat.Archive;
+ Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
+ {
+ Format = MangaFormat.Archive,
+ Name = "My Dress-Up Darling",
+ NormalizedName = API.Parser.Parser.Normalize("My Dress-Up Darling")
+ }));
+
+ Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
+ {
+ Format = MangaFormat.Archive,
+ Name = "Sono Bisque Doll wa Koi wo Suru".ToLower(),
+ NormalizedName = API.Parser.Parser.Normalize("Sono Bisque Doll wa Koi wo Suru")
+ }));
+ }
#endregion
[Fact]
diff --git a/API/API.csproj b/API/API.csproj
index a66d03dd6..a16a45180 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -50,11 +50,11 @@
-
-
-
+
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -66,13 +66,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs
index 958582338..e18bdb03f 100644
--- a/API/Controllers/BookController.cs
+++ b/API/Controllers/BookController.cs
@@ -94,6 +94,7 @@ namespace API.Controllers
///
///
[HttpGet("{chapterId}/book-resources")]
+ [ResponseCache(Duration = 60 * 1, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetBookPageResources(int chapterId, [FromQuery] string file)
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
@@ -105,7 +106,6 @@ namespace API.Controllers
var bookFile = book.Content.AllFiles[key];
var content = await bookFile.ReadContentAsBytesAsync();
- Response.AddCacheHeader(content);
var contentType = BookService.GetContentType(bookFile.ContentType);
return File(content, contentType, $"{chapterId}-{file}");
}
diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs
index d10478c49..8753202f8 100644
--- a/API/Controllers/DownloadController.cs
+++ b/API/Controllers/DownloadController.cs
@@ -83,7 +83,7 @@ namespace API.Controllers
///
- /// Downloads all chapters within a volume.
+ /// Downloads all chapters within a volume. If the chapters are multiple zips, they will all be zipped up.
///
///
///
@@ -112,12 +112,17 @@ namespace API.Controllers
return await _downloadService.HasDownloadPermission(user);
}
- private async Task GetFirstFileDownload(IEnumerable files)
+ private ActionResult GetFirstFileDownload(IEnumerable files)
{
- var (bytes, contentType, fileDownloadName) = await _downloadService.GetFirstFileDownload(files);
- return File(bytes, contentType, fileDownloadName);
+ var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
+ return PhysicalFile(zipFile, contentType, fileDownloadName, true);
}
+ ///
+ /// Returns the zip for a single chapter. If the chapter contains multiple files, they will be zipped.
+ ///
+ ///
+ ///
[HttpGet("chapter")]
public async Task DownloadChapter(int chapterId)
{
@@ -148,15 +153,14 @@ namespace API.Controllers
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
- return await GetFirstFileDownload(files);
+ return GetFirstFileDownload(files);
}
- var (fileBytes, _) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
- tempFolder);
+ var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder);
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
- return File(fileBytes, DefaultContentType, downloadName);
+ return PhysicalFile(filePath, DefaultContentType, downloadName, true);
}
catch (Exception ex)
{
@@ -184,10 +188,16 @@ namespace API.Controllers
}
}
+ ///
+ /// Downloads all bookmarks in a zip for
+ ///
+ ///
+ ///
[HttpPost("bookmarks")]
public async Task DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
+ if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
// We know that all bookmarks will be for one single seriesId
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
@@ -198,13 +208,14 @@ namespace API.Controllers
var filename = $"{series.Name} - Bookmarks.zip";
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 0F));
- var (fileBytes, _) = await _archiveService.CreateZipForDownload(files,
- $"download_{user.Id}_{series.Id}_bookmarks");
+ var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct());
+ var filePath = _archiveService.CreateZipForDownload(files,
+ $"download_{user.Id}_{seriesIds}_bookmarks");
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 1F));
- return File(fileBytes, DefaultContentType, filename);
+ return PhysicalFile(filePath, DefaultContentType, filename, true);
}
}
diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs
index b34b9f6b2..17ce2c215 100644
--- a/API/Controllers/ImageController.cs
+++ b/API/Controllers/ImageController.cs
@@ -2,7 +2,6 @@
using System.Threading.Tasks;
using API.Data;
using API.Entities.Enums;
-using API.Extensions;
using API.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -16,6 +15,7 @@ namespace API.Controllers
{
private readonly IUnitOfWork _unitOfWork;
private readonly IDirectoryService _directoryService;
+ private const int ImageCacheSeconds = 1 * 60;
///
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService)
@@ -30,13 +30,13 @@ namespace API.Controllers
///
///
[HttpGet("chapter-cover")]
+ [ResponseCache(Duration = ImageCacheSeconds, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetChapterCoverImage(int chapterId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
- Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
@@ -46,13 +46,13 @@ namespace API.Controllers
///
///
[HttpGet("volume-cover")]
+ [ResponseCache(Duration = ImageCacheSeconds, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetVolumeCoverImage(int volumeId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
- Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
@@ -61,6 +61,7 @@ namespace API.Controllers
///
/// Id of Series
///
+ [ResponseCache(Duration = ImageCacheSeconds, Location = ResponseCacheLocation.Client, NoStore = false)]
[HttpGet("series-cover")]
public async Task GetSeriesCoverImage(int seriesId)
{
@@ -68,7 +69,6 @@ namespace API.Controllers
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
- Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
@@ -78,13 +78,13 @@ namespace API.Controllers
///
///
[HttpGet("collection-cover")]
+ [ResponseCache(Duration = ImageCacheSeconds, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetCollectionCoverImage(int collectionTagId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
- Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
@@ -94,13 +94,13 @@ namespace API.Controllers
///
///
[HttpGet("readinglist-cover")]
+ [ResponseCache(Duration = ImageCacheSeconds, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetReadingListCoverImage(int readingListId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
- Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
@@ -113,6 +113,7 @@ namespace API.Controllers
/// API Key for user. Needed to authenticate request
///
[HttpGet("bookmark")]
+ [ResponseCache(Duration = ImageCacheSeconds, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetBookmarkImage(int chapterId, int pageNum, string apiKey)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
@@ -124,7 +125,6 @@ namespace API.Controllers
var file = new FileInfo(Path.Join(bookmarkDirectory, bookmark.FileName));
var format = Path.GetExtension(file.FullName).Replace(".", "");
- Response.AddCacheHeader(file.FullName);
return PhysicalFile(file.FullName, "image/" + format, Path.GetFileName(file.FullName));
}
@@ -135,13 +135,13 @@ namespace API.Controllers
///
[AllowAnonymous]
[HttpGet("cover-upload")]
+ [ResponseCache(Duration = ImageCacheSeconds, Location = ResponseCacheLocation.Client, NoStore = false)]
public ActionResult GetCoverUploadImage(string filename)
{
var path = Path.Join(_directoryService.TempDirectory, filename);
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
- Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
}
diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs
index 4dee326be..168ce974c 100644
--- a/API/Controllers/OPDSController.cs
+++ b/API/Controllers/OPDSController.cs
@@ -622,8 +622,8 @@ public class OpdsController : BaseApiController
}
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
- var (bytes, contentType, fileDownloadName) = await _downloadService.GetFirstFileDownload(files);
- return File(bytes, contentType, fileDownloadName);
+ var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
+ return PhysicalFile(zipFile, contentType, fileDownloadName, true);
}
private static ContentResult CreateXmlResult(string xml)
@@ -830,6 +830,7 @@ public class OpdsController : BaseApiController
}
[HttpGet("{apiKey}/favicon")]
+ [ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetFavicon(string apiKey)
{
var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico");
@@ -838,9 +839,6 @@ public class OpdsController : BaseApiController
var content = await _directoryService.ReadFileAsync(path);
var format = Path.GetExtension(path).Replace(".", "");
- // Calculates SHA1 Hash for byte[]
- Response.AddCacheHeader(content);
-
return File(content, "image/" + format);
}
diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs
index 34cb411bd..6cf02ffec 100644
--- a/API/Controllers/ReaderController.cs
+++ b/API/Controllers/ReaderController.cs
@@ -47,6 +47,7 @@ namespace API.Controllers
///
///
[HttpGet("pdf")]
+ [ResponseCache(Duration = 60 * 10, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetPdf(int chapterId)
{
var chapter = await _cacheService.Ensure(chapterId);
@@ -57,7 +58,6 @@ namespace API.Controllers
var path = _cacheService.GetCachedFile(chapter);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"Pdf doesn't exist when it should.");
- Response.AddCacheHeader(path, TimeSpan.FromMinutes(60).Seconds);
return PhysicalFile(path, "application/pdf", Path.GetFileName(path), true);
}
catch (Exception)
@@ -74,6 +74,7 @@ namespace API.Controllers
///
///
[HttpGet("image")]
+ [ResponseCache(Duration = 60 * 10, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetImage(int chapterId, int page)
{
if (page < 0) page = 0;
@@ -86,8 +87,7 @@ namespace API.Controllers
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
var format = Path.GetExtension(path).Replace(".", "");
- Response.AddCacheHeader(path, TimeSpan.FromMinutes(10).Seconds);
- return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
+ return PhysicalFile(path, "image/" + format, Path.GetFileName(path), true);
}
catch (Exception)
{
@@ -105,6 +105,7 @@ namespace API.Controllers
/// We must use api key as bookmarks could be leaked to other users via the API
///
[HttpGet("bookmark-image")]
+ [ResponseCache(Duration = 60 * 10, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task GetBookmarkImage(int seriesId, string apiKey, int page)
{
if (page < 0) page = 0;
@@ -121,7 +122,6 @@ namespace API.Controllers
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
var format = Path.GetExtension(path).Replace(".", "");
- Response.AddCacheHeader(path, TimeSpan.FromMinutes(10).Seconds);
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
}
catch (Exception)
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
index 76fa78fef..bc3acb1b8 100644
--- a/API/Controllers/SeriesController.cs
+++ b/API/Controllers/SeriesController.cs
@@ -253,30 +253,50 @@ namespace API.Controllers
return Ok(pagedList);
}
+ ///
+ /// Runs a Cover Image Generation task
+ ///
+ ///
+ ///
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("refresh-metadata")]
public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto)
{
- _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true);
+ _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
+ ///
+ /// Scan a series and force each file to be updated. This should be invoked via the User, hence why we force.
+ ///
+ ///
+ ///
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan")]
public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto)
{
- _taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId);
+ _taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
+ ///
+ /// Run a file analysis on the series.
+ ///
+ ///
+ ///
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("analyze")]
public ActionResult AnalyzeSeries(RefreshSeriesDto refreshSeriesDto)
{
- _taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true);
+ _taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
+ ///
+ /// Returns metadata for a given series
+ ///
+ ///
+ ///
[HttpGet("metadata")]
public async Task> GetSeriesMetadata(int seriesId)
{
@@ -284,6 +304,11 @@ namespace API.Controllers
return Ok(metadata);
}
+ ///
+ /// Update series metadata
+ ///
+ ///
+ ///
[HttpPost("metadata")]
public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
{
@@ -331,6 +356,11 @@ namespace API.Controllers
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
}
+ ///
+ /// Get the age rating for the enum value
+ ///
+ ///
+ ///
[HttpGet("age-rating")]
public ActionResult GetAgeRating(int ageRating)
{
@@ -339,6 +369,12 @@ namespace API.Controllers
return Ok(val.ToDescription());
}
+ ///
+ /// Get a special DTO for Series Detail page.
+ ///
+ ///
+ ///
+ /// Do not rely on this API externally. May change without hesitation.
[HttpGet("series-detail")]
public async Task> GetSeriesDetailBreakdown(int seriesId)
{
@@ -386,16 +422,24 @@ namespace API.Controllers
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
}
+ ///
+ /// Returns all related series against the passed series Id
+ ///
+ ///
+ ///
[HttpGet("all-related")]
public async Task> GetAllRelatedSeries(int seriesId)
{
- // Send back a custom DTO with each type or maybe sorted in some way
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
}
-
+ ///
+ /// Update the relations attached to the Series. Does not generate associated Sequel/Prequel pairs on target series.
+ ///
+ ///
+ ///
[Authorize(Policy="RequireAdminRole")]
[HttpPost("update-related")]
public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
@@ -421,7 +465,8 @@ namespace API.Controllers
return BadRequest("There was an issue updating relationships");
}
- private void UpdateRelationForKind(IList dtoTargetSeriesIds, IEnumerable adaptations, Series series, RelationKind kind)
+ // TODO: Move this to a Service and Unit Test it
+ private void UpdateRelationForKind(ICollection dtoTargetSeriesIds, IEnumerable adaptations, Series series, RelationKind kind)
{
foreach (var adaptation in adaptations.Where(adaptation => !dtoTargetSeriesIds.Contains(adaptation.TargetSeriesId)))
{
diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs
index f49a7e092..161541a24 100644
--- a/API/Controllers/ServerController.cs
+++ b/API/Controllers/ServerController.cs
@@ -112,13 +112,13 @@ namespace API.Controllers
}
[HttpGet("logs")]
- public async Task GetLogs()
+ public ActionResult GetLogs()
{
var files = _backupService.GetLogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName());
try
{
- var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files, "logs");
- return File(fileBytes, "application/zip", Path.GetFileName(zipPath), true);
+ var zipPath = _archiveService.CreateZipForDownload(files, "logs");
+ return PhysicalFile(zipPath, "application/zip", Path.GetFileName(zipPath), true);
}
catch (KavitaException ex)
{
diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs
index 7b7cf492d..ce8753157 100644
--- a/API/Controllers/UsersController.cs
+++ b/API/Controllers/UsersController.cs
@@ -98,6 +98,7 @@ namespace API.Controllers
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
+ existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
_unitOfWork.UserRepository.Update(existingPreferences);
diff --git a/API/DTOs/Downloads/DownloadBookmarkDto.cs b/API/DTOs/Downloads/DownloadBookmarkDto.cs
index b7ccf9569..b1158ff23 100644
--- a/API/DTOs/Downloads/DownloadBookmarkDto.cs
+++ b/API/DTOs/Downloads/DownloadBookmarkDto.cs
@@ -1,10 +1,12 @@
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using API.DTOs.Reader;
namespace API.DTOs.Downloads
{
public class DownloadBookmarkDto
{
+ [Required]
public IEnumerable Bookmarks { get; set; }
}
}
diff --git a/API/DTOs/ProgressDto.cs b/API/DTOs/ProgressDto.cs
index 4810a40a9..021a5f243 100644
--- a/API/DTOs/ProgressDto.cs
+++ b/API/DTOs/ProgressDto.cs
@@ -1,10 +1,16 @@
-namespace API.DTOs
+using System.ComponentModel.DataAnnotations;
+
+namespace API.DTOs
{
public class ProgressDto
{
+ [Required]
public int VolumeId { get; set; }
+ [Required]
public int ChapterId { get; set; }
+ [Required]
public int PageNum { get; set; }
+ [Required]
public int SeriesId { get; set; }
///
/// For Book reader, this can be an optional string of the id of a part marker, to help resume reading position
diff --git a/API/DTOs/RefreshSeriesDto.cs b/API/DTOs/RefreshSeriesDto.cs
index bc6344ea2..db1264bd3 100644
--- a/API/DTOs/RefreshSeriesDto.cs
+++ b/API/DTOs/RefreshSeriesDto.cs
@@ -1,8 +1,22 @@
namespace API.DTOs
{
+ ///
+ /// Used for running some task against a Series.
+ ///
public class RefreshSeriesDto
{
- public int LibraryId { get; set; }
- public int SeriesId { get; set; }
+ ///
+ /// Library Id series belongs to
+ ///
+ public int LibraryId { get; init; }
+ ///
+ /// Series Id
+ ///
+ public int SeriesId { get; init; }
+ ///
+ /// Should the task force opening/re-calculation.
+ ///
+ /// This is expensive if true. Defaults to true.
+ public bool ForceUpdate { get; init; } = true;
}
-}
\ No newline at end of file
+}
diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs
index 063b07726..255c21c1f 100644
--- a/API/DTOs/UserPreferencesDto.cs
+++ b/API/DTOs/UserPreferencesDto.cs
@@ -1,4 +1,6 @@
-using API.DTOs.Theme;
+using System;
+using System.ComponentModel.DataAnnotations;
+using API.DTOs.Theme;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Enums.UserPreferences;
@@ -10,14 +12,17 @@ namespace API.DTOs
///
/// Manga Reader Option: What direction should the next/prev page buttons go
///
+ [Required]
public ReadingDirection ReadingDirection { get; set; }
///
/// Manga Reader Option: How should the image be scaled to screen
///
+ [Required]
public ScalingOption ScalingOption { get; set; }
///
/// Manga Reader Option: Which side of a split image should we show first
///
+ [Required]
public PageSplitOption PageSplitOption { get; set; }
///
/// Manga Reader Option: How the manga reader should perform paging or reading of the file
@@ -26,72 +31,90 @@ namespace API.DTOs
/// by clicking top/bottom sides of reader.
///
///
+ [Required]
public ReaderMode ReaderMode { get; set; }
///
/// Manga Reader Option: How many pages to display in the reader at once
///
+ [Required]
public LayoutMode LayoutMode { get; set; }
///
/// Manga Reader Option: Background color of the reader
///
+ [Required]
public string BackgroundColor { get; set; } = "#000000";
///
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
///
+ [Required]
public bool AutoCloseMenu { get; set; }
///
/// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change
///
+ [Required]
public bool ShowScreenHints { get; set; } = true;
///
- /// Book Reader Option: Should the background color be dark
- ///
- public bool BookReaderDarkMode { get; set; } = false;
- ///
/// Book Reader Option: Override extra Margin
///
+ [Required]
public int BookReaderMargin { get; set; }
///
/// Book Reader Option: Override line-height
///
+ [Required]
public int BookReaderLineSpacing { get; set; }
///
/// Book Reader Option: Override font size
///
+ [Required]
public int BookReaderFontSize { get; set; }
///
/// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override
///
+ [Required]
public string BookReaderFontFamily { get; set; }
///
/// Book Reader Option: Allows tapping on side of screens to paginate
///
+ [Required]
public bool BookReaderTapToPaginate { get; set; }
///
/// Book Reader Option: What direction should the next/prev page buttons go
///
+ [Required]
public ReadingDirection BookReaderReadingDirection { get; set; }
///
/// UI Site Global Setting: The UI theme the user should use.
///
/// Should default to Dark
+ [Required]
public SiteTheme Theme { get; set; }
+ [Required]
public string BookReaderThemeName { get; set; }
+ [Required]
public BookPageLayoutMode BookReaderLayoutMode { get; set; }
///
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
///
/// Defaults to false
+ [Required]
public bool BookReaderImmersiveMode { get; set; } = false;
///
/// Global Site Option: If the UI should layout items as Cards or List items
///
/// Defaults to Cards
+ [Required]
public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards;
///
/// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already
///
/// Defaults to false
+ [Required]
public bool BlurUnreadSummaries { get; set; } = false;
+ ///
+ /// UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB.
+ ///
+ [Required]
+ public bool PromptForDownloadSize { get; set; } = true;
}
}
diff --git a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs b/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs
new file mode 100644
index 000000000..a2eb08e68
--- /dev/null
+++ b/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs
@@ -0,0 +1,1576 @@
+//
+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("20220712161611_PromptForDownloadSizeUserOption")]
+ partial class PromptForDownloadSizeUserOption
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "6.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("ApiKey")
+ .HasColumnType("TEXT");
+
+ 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", (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("FileName")
+ .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.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("BookThemeName")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("Dark");
+
+ b.Property("GlobalPageLayoutMode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("PromptForDownloadSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowScreenHints")
+ .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("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.HasIndex("SeriesId");
+
+ 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.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.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("MaxHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("MinHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("TitleName")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("WordCount")
+ .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("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .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.Genre", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ExternalTag")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedTitle", "ExternalTag")
+ .IsUnique();
+
+ b.ToTable("Genre");
+ });
+
+ 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("LastScanned")
+ .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("LastFileAnalysis")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.ToTable("MangaFile");
+ });
+
+ modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRatingLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("CharacterLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ColoristLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverArtistLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("EditorLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("GenresLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("InkerLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LanguageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("LettererLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("PencillerLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublicationStatus")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublicationStatusLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublisherLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReleaseYear")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("SummaryLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("TagsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("TotalCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("TranslatorLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("WriterLocked")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId")
+ .IsUnique();
+
+ b.HasIndex("Id", "SeriesId")
+ .IsUnique();
+
+ b.ToTable("SeriesMetadata");
+ });
+
+ modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("RelationKind")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TargetSeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId");
+
+ b.HasIndex("TargetSeriesId");
+
+ b.ToTable("SeriesRelation");
+ });
+
+ modelBuilder.Entity("API.Entities.Person", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Role")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Person");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingList", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("ReadingList");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingListItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingListId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("ReadingListId");
+
+ b.HasIndex("SeriesId");
+
+ b.HasIndex("VolumeId");
+
+ b.ToTable("ReadingListItem");
+ });
+
+ modelBuilder.Entity("API.Entities.Series", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastChapterAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LocalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("LocalizedNameLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("MinHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NameLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalName")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property("SortNameLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("WordCount")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("Series");
+ });
+
+ 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.SiteTheme", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Provider")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("SiteTheme");
+ });
+
+ modelBuilder.Entity("API.Entities.Tag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ExternalTag")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedTitle", "ExternalTag")
+ .IsUnique();
+
+ b.ToTable("Tag");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("MaxHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("MinHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("INTEGER");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("WordCount")
+ .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("ChapterGenre", b =>
+ {
+ b.Property("ChaptersId")
+ .HasColumnType("INTEGER");
+
+ b.Property("GenresId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ChaptersId", "GenresId");
+
+ b.HasIndex("GenresId");
+
+ b.ToTable("ChapterGenre");
+ });
+
+ modelBuilder.Entity("ChapterPerson", b =>
+ {
+ b.Property("ChapterMetadatasId")
+ .HasColumnType("INTEGER");
+
+ b.Property