mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Misc Bugfixes and Enhancements (#507)
* Removed some extra spam for the console * Implemented the code to update RowVersion, which is our concurrency check * Fixed a critical issue where more than one bookmark could occur for a given chapter due to a race condition. Now we use concurrency checks and we also gracefully allow more than one row, by only grabbing first. * Cleaned up the logic for IHasConcurencyToken and updated the setters to be private. * Lots of comments and when deleting a library, remove any user progress items for which chapters don't exist. * When deleting a Series, cleanup user progress rows. * Now after a scan of library, if a series is removed, collection tags are pruned as well if there are no longer any series bound to it. * Updated the image on the Readme to show a better picture * Small code cleanup to remove null check modifier as I check for null just before then * Fixed images loading multiple times due to using function in binding with random. You can now click chapter images to read that chapter specifically. * Fixed cards being different sizes when read vs unread * Moved over Robbie's workflow changes from notifier. Commented out activity indicators as that is not shipping with this release. * Remove code that isn't needed * Reverted GA * Changed GA to trigger only when HEAD is updated
This commit is contained in:
parent
16faa82d20
commit
3ed99afd32
16
.github/workflows/sonar-scan.yml
vendored
16
.github/workflows/sonar-scan.yml
vendored
@ -5,7 +5,7 @@ on:
|
|||||||
branches: '**'
|
branches: '**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, develop ]
|
branches: [ main, develop ]
|
||||||
types: [opened, synchronize, reopened]
|
types: [synchronize]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -98,7 +98,7 @@ jobs:
|
|||||||
needs: [ build, test ]
|
needs: [ build, test ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@ -122,7 +122,7 @@ jobs:
|
|||||||
|
|
||||||
develop:
|
develop:
|
||||||
name: Build Nightly Docker if Develop push
|
name: Build Nightly Docker if Develop push
|
||||||
needs: [ build, test, version ]
|
needs: [ build, test, version ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||||
steps:
|
steps:
|
||||||
@ -222,7 +222,7 @@ jobs:
|
|||||||
|
|
||||||
stable:
|
stable:
|
||||||
name: Build Stable Docker if Main push
|
name: Build Stable Docker if Main push
|
||||||
needs: [ build, test ]
|
needs: [ build, test ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
steps:
|
steps:
|
||||||
@ -263,9 +263,9 @@ jobs:
|
|||||||
|
|
||||||
echo 'Copying back to Kavita wwwroot'
|
echo 'Copying back to Kavita wwwroot'
|
||||||
rsync -a dist/ ../../API/wwwroot/
|
rsync -a dist/ ../../API/wwwroot/
|
||||||
|
|
||||||
cd ../ || exit
|
cd ../ || exit
|
||||||
|
|
||||||
- name: Get csproj Version
|
- name: Get csproj Version
|
||||||
uses: naminodarie/get-net-sdk-project-versions-action@v1
|
uses: naminodarie/get-net-sdk-project-versions-action@v1
|
||||||
id: get-version
|
id: get-version
|
||||||
@ -281,7 +281,7 @@ jobs:
|
|||||||
newVersion=${version%.*}
|
newVersion=${version%.*}
|
||||||
echo $newVersion
|
echo $newVersion
|
||||||
echo "::set-output name=VERSION::$newVersion"
|
echo "::set-output name=VERSION::$newVersion"
|
||||||
id: parse-version
|
id: parse-version
|
||||||
|
|
||||||
- name: Compile dotnet app
|
- name: Compile dotnet app
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
@ -320,7 +320,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Image digest
|
- name: Image digest
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
||||||
- name: Notify Discord
|
- name: Notify Discord
|
||||||
uses: rjstone/discord-webhook-notify@v1
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
with:
|
with:
|
||||||
|
@ -275,6 +275,7 @@ namespace API.Tests.Parser
|
|||||||
Assert.Equal(expected, API.Parser.Parser.ParseMangaSpecial(inputFile));
|
Assert.Equal(expected, API.Parser.Parser.ParseMangaSpecial(inputFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
private static ParserInfo CreateParserInfo(string series, string chapter, string volume, bool isSpecial = false)
|
private static ParserInfo CreateParserInfo(string series, string chapter, string volume, bool isSpecial = false)
|
||||||
{
|
{
|
||||||
return new ParserInfo()
|
return new ParserInfo()
|
||||||
@ -285,6 +286,7 @@ namespace API.Tests.Parser
|
|||||||
Series = series,
|
Series = series,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")]
|
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")]
|
||||||
|
@ -5,8 +5,17 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public enum ArchiveLibrary
|
public enum ArchiveLibrary
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying archive cannot be opened
|
||||||
|
/// </summary>
|
||||||
NotSupported = 0,
|
NotSupported = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying archive can be opened by SharpCompress
|
||||||
|
/// </summary>
|
||||||
SharpCompress = 1,
|
SharpCompress = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying archive can be opened by default .NET
|
||||||
|
/// </summary>
|
||||||
Default = 2
|
Default = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
namespace API.Archive
|
|
||||||
{
|
|
||||||
public class CoverAndPages
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,21 @@
|
|||||||
namespace API.Constants
|
namespace API.Constants
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Role-based Security
|
||||||
|
/// </summary>
|
||||||
public static class PolicyConstants
|
public static class PolicyConstants
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Admin User. Has all privileges
|
||||||
|
/// </summary>
|
||||||
public const string AdminRole = "Admin";
|
public const string AdminRole = "Admin";
|
||||||
|
/// <summary>
|
||||||
|
/// Non-Admin User. Must be granted privileges by an Admin.
|
||||||
|
/// </summary>
|
||||||
public const string PlebRole = "Pleb";
|
public const string PlebRole = "Pleb";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to give a user ability to download files from the server
|
/// Used to give a user ability to download files from the server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string DownloadRole = "Download";
|
public const string DownloadRole = "Download";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,9 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All Account matters
|
||||||
|
/// </summary>
|
||||||
public class AccountController : BaseApiController
|
public class AccountController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
@ -27,9 +30,10 @@ namespace API.Controllers
|
|||||||
private readonly ILogger<AccountController> _logger;
|
private readonly ILogger<AccountController> _logger;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public AccountController(UserManager<AppUser> userManager,
|
public AccountController(UserManager<AppUser> userManager,
|
||||||
SignInManager<AppUser> signInManager,
|
SignInManager<AppUser> signInManager,
|
||||||
ITokenService tokenService, IUnitOfWork unitOfWork,
|
ITokenService tokenService, IUnitOfWork unitOfWork,
|
||||||
ILogger<AccountController> logger,
|
ILogger<AccountController> logger,
|
||||||
IMapper mapper)
|
IMapper mapper)
|
||||||
{
|
{
|
||||||
@ -40,7 +44,12 @@ namespace API.Controllers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update a user's password
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resetPasswordDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("reset-password")]
|
[HttpPost("reset-password")]
|
||||||
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
||||||
{
|
{
|
||||||
@ -49,7 +58,7 @@ namespace API.Controllers
|
|||||||
|
|
||||||
if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
|
if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole))
|
||||||
return Unauthorized("You are not permitted to this operation.");
|
return Unauthorized("You are not permitted to this operation.");
|
||||||
|
|
||||||
// Validate Password
|
// Validate Password
|
||||||
foreach (var validator in _userManager.PasswordValidators)
|
foreach (var validator in _userManager.PasswordValidators)
|
||||||
{
|
{
|
||||||
@ -60,26 +69,31 @@ namespace API.Controllers
|
|||||||
validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _userManager.RemovePasswordAsync(user);
|
var result = await _userManager.RemovePasswordAsync(user);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogError("Could not update password");
|
_logger.LogError("Could not update password");
|
||||||
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
result = await _userManager.AddPasswordAsync(user, resetPasswordDto.Password);
|
result = await _userManager.AddPasswordAsync(user, resetPasswordDto.Password);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogError("Could not update password");
|
_logger.LogError("Could not update password");
|
||||||
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
return BadRequest(result.Errors.Select(e => new ApiException(400, e.Code, e.Description)));
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
|
_logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a new user on the server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registerDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
|
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
|
||||||
{
|
{
|
||||||
@ -134,6 +148,11 @@ namespace API.Controllers
|
|||||||
return BadRequest("Something went wrong when registering user");
|
return BadRequest("Something went wrong when registering user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform a login. Will send JWT Token of the logged in user back.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loginDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
||||||
{
|
{
|
||||||
@ -147,14 +166,14 @@ namespace API.Controllers
|
|||||||
.CheckPasswordSignInAsync(user, loginDto.Password, false);
|
.CheckPasswordSignInAsync(user, loginDto.Password, false);
|
||||||
|
|
||||||
if (!result.Succeeded) return Unauthorized("Your credentials are not correct.");
|
if (!result.Succeeded) return Unauthorized("Your credentials are not correct.");
|
||||||
|
|
||||||
// Update LastActive on account
|
// Update LastActive on account
|
||||||
user.LastActive = DateTime.Now;
|
user.LastActive = DateTime.Now;
|
||||||
user.UserPreferences ??= new AppUserPreferences();
|
user.UserPreferences ??= new AppUserPreferences();
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
_logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive);
|
_logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive);
|
||||||
|
|
||||||
return new UserDto
|
return new UserDto
|
||||||
@ -165,6 +184,10 @@ namespace API.Controllers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get All Roles back. See <see cref="PolicyConstants"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("roles")]
|
[HttpGet("roles")]
|
||||||
public ActionResult<IList<string>> GetRoles()
|
public ActionResult<IList<string>> GetRoles()
|
||||||
{
|
{
|
||||||
@ -175,6 +198,11 @@ namespace API.Controllers
|
|||||||
f => (string) f.GetValue(null)).Values.ToList();
|
f => (string) f.GetValue(null)).Values.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the given roles to the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="updateRbsDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("update-rbs")]
|
[HttpPost("update-rbs")]
|
||||||
public async Task<ActionResult> UpdateRoles(UpdateRbsDto updateRbsDto)
|
public async Task<ActionResult> UpdateRoles(UpdateRbsDto updateRbsDto)
|
||||||
{
|
{
|
||||||
@ -190,7 +218,7 @@ namespace API.Controllers
|
|||||||
var existingRoles = (await _userManager.GetRolesAsync(user))
|
var existingRoles = (await _userManager.GetRolesAsync(user))
|
||||||
.Where(s => s != PolicyConstants.AdminRole && s != PolicyConstants.PlebRole)
|
.Where(s => s != PolicyConstants.AdminRole && s != PolicyConstants.PlebRole)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Find what needs to be added and what needs to be removed
|
// Find what needs to be added and what needs to be removed
|
||||||
var rolesToRemove = existingRoles.Except(updateRbsDto.Roles);
|
var rolesToRemove = existingRoles.Except(updateRbsDto.Roles);
|
||||||
var result = await _userManager.AddToRolesAsync(user, updateRbsDto.Roles);
|
var result = await _userManager.AddToRolesAsync(user, updateRbsDto.Roles);
|
||||||
@ -204,10 +232,10 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _unitOfWork.RollbackAsync();
|
await _unitOfWork.RollbackAsync();
|
||||||
return BadRequest("Something went wrong, unable to update user's roles");
|
return BadRequest("Something went wrong, unable to update user's roles");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,6 +185,8 @@ namespace API.Controllers
|
|||||||
|
|
||||||
if (chapterIds.Any())
|
if (chapterIds.Any())
|
||||||
{
|
{
|
||||||
|
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
_taskScheduler.CleanupChapters(chapterIds);
|
_taskScheduler.CleanupChapters(chapterIds);
|
||||||
}
|
}
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
|
@ -14,6 +14,9 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// For all things regarding reading, mainly focusing on non-Book related entities
|
||||||
|
/// </summary>
|
||||||
public class ReaderController : BaseApiController
|
public class ReaderController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
@ -23,6 +26,7 @@ namespace API.Controllers
|
|||||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||||
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork)
|
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork)
|
||||||
{
|
{
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
@ -30,6 +34,12 @@ namespace API.Controllers
|
|||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an image for a given chapter. Side effect: This will cache the chapter images for reading.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <param name="page"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("image")]
|
[HttpGet("image")]
|
||||||
public async Task<ActionResult> GetImage(int chapterId, int page)
|
public async Task<ActionResult> GetImage(int chapterId, int page)
|
||||||
{
|
{
|
||||||
@ -57,6 +67,12 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("chapter-info")]
|
[HttpGet("chapter-info")]
|
||||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int seriesId, int chapterId)
|
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int seriesId, int chapterId)
|
||||||
{
|
{
|
||||||
@ -149,6 +165,11 @@ namespace API.Controllers
|
|||||||
return userProgress;
|
return userProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a Chapter as Unread (progress)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="markReadDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("mark-unread")]
|
[HttpPost("mark-unread")]
|
||||||
public async Task<ActionResult> MarkUnread(MarkReadDto markReadDto)
|
public async Task<ActionResult> MarkUnread(MarkReadDto markReadDto)
|
||||||
{
|
{
|
||||||
@ -179,6 +200,11 @@ namespace API.Controllers
|
|||||||
return BadRequest("There was an issue saving progress");
|
return BadRequest("There was an issue saving progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks all chapters within a volume as Read
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="markVolumeReadDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("mark-volume-read")]
|
[HttpPost("mark-volume-read")]
|
||||||
public async Task<ActionResult> MarkVolumeAsRead(MarkVolumeReadDto markVolumeReadDto)
|
public async Task<ActionResult> MarkVolumeAsRead(MarkVolumeReadDto markVolumeReadDto)
|
||||||
{
|
{
|
||||||
@ -218,6 +244,11 @@ namespace API.Controllers
|
|||||||
return BadRequest("Could not save progress");
|
return BadRequest("Could not save progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns Progress (page number) for a chapter for the logged in user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("get-progress")]
|
[HttpGet("get-progress")]
|
||||||
public async Task<ActionResult<ProgressDto>> GetProgress(int chapterId)
|
public async Task<ActionResult<ProgressDto>> GetProgress(int chapterId)
|
||||||
{
|
{
|
||||||
@ -242,6 +273,11 @@ namespace API.Controllers
|
|||||||
return Ok(progressBookmark);
|
return Ok(progressBookmark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save page against Chapter for logged in user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="progressDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("progress")]
|
[HttpPost("progress")]
|
||||||
public async Task<ActionResult> BookmarkProgress(ProgressDto progressDto)
|
public async Task<ActionResult> BookmarkProgress(ProgressDto progressDto)
|
||||||
{
|
{
|
||||||
@ -259,13 +295,11 @@ namespace API.Controllers
|
|||||||
progressDto.PageNum = 0;
|
progressDto.PageNum = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// TODO: Look into creating a progress entry when a new item is added to the DB so we can just look it up and modify it
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
user.Progresses ??= new List<AppUserProgress>();
|
|
||||||
var userProgress =
|
var userProgress =
|
||||||
user.Progresses.SingleOrDefault(x => x.ChapterId == progressDto.ChapterId && x.AppUserId == user.Id);
|
user.Progresses.FirstOrDefault(x => x.ChapterId == progressDto.ChapterId && x.AppUserId == user.Id);
|
||||||
|
|
||||||
if (userProgress == null)
|
if (userProgress == null)
|
||||||
{
|
{
|
||||||
@ -303,6 +337,11 @@ namespace API.Controllers
|
|||||||
return BadRequest("Could not save progress");
|
return BadRequest("Could not save progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of bookmarked pages for a given Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("get-bookmarks")]
|
[HttpGet("get-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||||
{
|
{
|
||||||
@ -311,6 +350,11 @@ namespace API.Controllers
|
|||||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all bookmarks for all chapters linked to a Series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("remove-bookmarks")]
|
[HttpPost("remove-bookmarks")]
|
||||||
public async Task<ActionResult> RemoveBookmarks(int seriesId)
|
public async Task<ActionResult> RemoveBookmarks(int seriesId)
|
||||||
{
|
{
|
||||||
@ -335,6 +379,11 @@ namespace API.Controllers
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all bookmarked pages for a given volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volumeId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("get-volume-bookmarks")]
|
[HttpGet("get-volume-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||||
{
|
{
|
||||||
@ -343,6 +392,11 @@ namespace API.Controllers
|
|||||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId));
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all bookmarked pages for a given series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("get-series-bookmarks")]
|
[HttpGet("get-series-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||||
{
|
{
|
||||||
@ -352,6 +406,11 @@ namespace API.Controllers
|
|||||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId));
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bookmarks a page against a Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bookmarkDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("bookmark")]
|
[HttpPost("bookmark")]
|
||||||
public async Task<ActionResult> BookmarkPage(BookmarkDto bookmarkDto)
|
public async Task<ActionResult> BookmarkPage(BookmarkDto bookmarkDto)
|
||||||
{
|
{
|
||||||
@ -408,6 +467,11 @@ namespace API.Controllers
|
|||||||
return BadRequest("Could not save bookmark");
|
return BadRequest("Could not save bookmark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a bookmarked page for a Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bookmarkDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("unbookmark")]
|
[HttpPost("unbookmark")]
|
||||||
public async Task<ActionResult> UnBookmarkPage(BookmarkDto bookmarkDto)
|
public async Task<ActionResult> UnBookmarkPage(BookmarkDto bookmarkDto)
|
||||||
{
|
{
|
||||||
|
@ -79,6 +79,9 @@ namespace API.Controllers
|
|||||||
|
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
|
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
||||||
|
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
_taskScheduler.CleanupChapters(chapterIds);
|
_taskScheduler.CleanupChapters(chapterIds);
|
||||||
}
|
}
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
|
@ -25,12 +25,26 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
_context.CollectionTag.Remove(tag);
|
_context.CollectionTag.Remove(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(CollectionTag tag)
|
public void Update(CollectionTag tag)
|
||||||
{
|
{
|
||||||
_context.Entry(tag).State = EntityState.Modified;
|
_context.Entry(tag).State = EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes any collection tags without any series
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> RemoveTagsWithoutSeries()
|
||||||
|
{
|
||||||
|
var tagsToDelete = await _context.CollectionTag
|
||||||
|
.Include(c => c.SeriesMetadatas)
|
||||||
|
.Where(c => c.SeriesMetadatas.Count == 0)
|
||||||
|
.ToListAsync();
|
||||||
|
_context.RemoveRange(tagsToDelete);
|
||||||
|
|
||||||
|
return await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
||||||
{
|
{
|
||||||
return await _context.CollectionTag
|
return await _context.CollectionTag
|
||||||
@ -40,7 +54,7 @@ namespace API.Data
|
|||||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync()
|
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync()
|
||||||
{
|
{
|
||||||
return await _context.CollectionTag
|
return await _context.CollectionTag
|
||||||
@ -57,7 +71,7 @@ namespace API.Data
|
|||||||
.Where(c => c.Id == tagId)
|
.Where(c => c.Id == tagId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CollectionTag> GetFullTagAsync(int tagId)
|
public async Task<CollectionTag> GetFullTagAsync(int tagId)
|
||||||
{
|
{
|
||||||
return await _context.CollectionTag
|
return await _context.CollectionTag
|
||||||
@ -69,7 +83,7 @@ namespace API.Data
|
|||||||
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery)
|
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery)
|
||||||
{
|
{
|
||||||
return await _context.CollectionTag
|
return await _context.CollectionTag
|
||||||
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|
||||||
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
|
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
|
||||||
.OrderBy(s => s.Title)
|
.OrderBy(s => s.Title)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@ -87,4 +101,4 @@ namespace API.Data
|
|||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@ -67,5 +70,48 @@ namespace API.Data
|
|||||||
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
||||||
entity.LastModified = DateTime.Now;
|
entity.LastModified = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSaveChanges()
|
||||||
|
{
|
||||||
|
foreach (var saveEntity in ChangeTracker.Entries()
|
||||||
|
.Where(e => e.State == EntityState.Modified)
|
||||||
|
.Select(entry => entry.Entity)
|
||||||
|
.OfType<IHasConcurrencyToken>())
|
||||||
|
{
|
||||||
|
saveEntity.OnSavingChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region SaveChanges overrides
|
||||||
|
|
||||||
|
public override int SaveChanges()
|
||||||
|
{
|
||||||
|
this.OnSaveChanges();
|
||||||
|
|
||||||
|
return base.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||||
|
{
|
||||||
|
this.OnSaveChanges();
|
||||||
|
|
||||||
|
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
this.OnSaveChanges();
|
||||||
|
|
||||||
|
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
this.OnSaveChanges();
|
||||||
|
|
||||||
|
return base.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
926
API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs
generated
Normal file
926
API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs
generated
Normal file
@ -0,0 +1,926 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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("20210817152226_ProgressConcurencyCheck")]
|
||||||
|
partial class ProgressConcurencyCheck
|
||||||
|
{
|
||||||
|
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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastActive")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Page")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserBookmark");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoCloseMenu")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookReaderFontFamily")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderFontSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderLineSpacing")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderMargin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("BookReaderTapToPaginate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PageSplitOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReaderMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ReadingDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScalingOption")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("SiteDarkMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AppUserPreferences");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookScrollId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("PagesRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserProgresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Review")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserRating");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpecial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Number")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Range")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("VolumeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VolumeId");
|
||||||
|
|
||||||
|
b.ToTable("Chapter");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedTitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Promoted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Id", "Promoted")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("CollectionTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastScanned")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
|
b.ToTable("MangaFile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<bool>("CoverImageLocked")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LocalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SortName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Key")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("ServerSetting");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("CoverImage")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Number")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Pages")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AppUsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibrariesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("AppUsersId", "LibrariesId");
|
||||||
|
|
||||||
|
b.HasIndex("LibrariesId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserLibrary");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CollectionTagsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesMetadatasId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CollectionTagsId", "SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
|
b.ToTable("CollectionTagSeriesMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class ProgressConcurencyCheck : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<uint>(
|
||||||
|
name: "RowVersion",
|
||||||
|
table: "AppUserProgresses",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RowVersion",
|
||||||
|
table: "AppUserProgresses");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -226,6 +226,10 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("PagesRead")
|
b.Property<int>("PagesRead")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("SeriesId")
|
b.Property<int>("SeriesId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
@ -33,25 +33,46 @@ namespace API.Data
|
|||||||
public IFileRepository FileRepository => new FileRepository(_context);
|
public IFileRepository FileRepository => new FileRepository(_context);
|
||||||
public IChapterRepository ChapterRepository => new ChapterRepository(_context);
|
public IChapterRepository ChapterRepository => new ChapterRepository(_context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Commits changes to the DB. Completes the open transaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public bool Commit()
|
public bool Commit()
|
||||||
{
|
{
|
||||||
|
|
||||||
return _context.SaveChanges() > 0;
|
return _context.SaveChanges() > 0;
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Commits changes to the DB. Completes the open transaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task<bool> CommitAsync()
|
public async Task<bool> CommitAsync()
|
||||||
{
|
{
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the DB Context aware of Changes in loaded entities
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public bool HasChanges()
|
public bool HasChanges()
|
||||||
{
|
{
|
||||||
return _context.ChangeTracker.HasChanges();
|
return _context.ChangeTracker.HasChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rollback transaction
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task<bool> RollbackAsync()
|
public async Task<bool> RollbackAsync()
|
||||||
{
|
{
|
||||||
await _context.DisposeAsync();
|
await _context.DisposeAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Rollback transaction
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public bool Rollback()
|
public bool Rollback()
|
||||||
{
|
{
|
||||||
_context.Dispose();
|
_context.Dispose();
|
||||||
|
@ -18,9 +18,11 @@ namespace API.Entities
|
|||||||
public AppUserPreferences UserPreferences { get; set; }
|
public AppUserPreferences UserPreferences { get; set; }
|
||||||
public ICollection<AppUserBookmark> Bookmarks { get; set; }
|
public ICollection<AppUserBookmark> Bookmarks { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
|
@ -1,30 +1,71 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the progress a single user has on a given Chapter.
|
/// Represents the progress a single user has on a given Chapter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppUserProgress : IEntityDate
|
//[Index(nameof(SeriesId), nameof(VolumeId), nameof(ChapterId), nameof(AppUserId), IsUnique = true)]
|
||||||
|
public class AppUserProgress : IEntityDate, IHasConcurrencyToken
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of Entity
|
||||||
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Pages Read for given Chapter
|
||||||
|
/// </summary>
|
||||||
public int PagesRead { get; set; }
|
public int PagesRead { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Volume belonging to Chapter
|
||||||
|
/// </summary>
|
||||||
public int VolumeId { get; set; }
|
public int VolumeId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Series belonging to Chapter
|
||||||
|
/// </summary>
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Chapter
|
||||||
|
/// </summary>
|
||||||
public int ChapterId { get; set; }
|
public int ChapterId { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point
|
/// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point
|
||||||
/// on next load
|
/// on next load
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BookScrollId { get; set; }
|
public string BookScrollId { get; set; }
|
||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
|
/// <summary>
|
||||||
|
/// Navigational Property for EF. Links to a unique AppUser
|
||||||
|
/// </summary>
|
||||||
public AppUser AppUser { get; set; }
|
public AppUser AppUser { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// User this progress belongs to
|
||||||
|
/// </summary>
|
||||||
public int AppUserId { get; set; }
|
public int AppUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When this was first created
|
||||||
|
/// </summary>
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Last date this was updated
|
||||||
|
/// </summary>
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[ConcurrencyCheck]
|
||||||
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnSavingChanges()
|
||||||
|
{
|
||||||
|
RowVersion++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,9 +43,11 @@ namespace API.Entities
|
|||||||
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; }
|
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
|
@ -9,9 +9,11 @@ namespace API.Entities
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
// MetadataUpdate add ProviderId
|
// MetadataUpdate add ProviderId
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
|
@ -10,12 +10,14 @@ namespace API.Entities
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public PersonRole Role { get; set; }
|
public PersonRole Role { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,11 @@ namespace API.Entities
|
|||||||
public Series Series { get; set; }
|
public Series Series { get; set; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
|
@ -10,11 +10,14 @@ namespace API.Entities
|
|||||||
public ServerSettingKey Key { get; set; }
|
public ServerSettingKey Key { get; set; }
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,5 +15,6 @@ namespace API.Interfaces
|
|||||||
Task<CollectionTag> GetTagAsync(int tagId);
|
Task<CollectionTag> GetTagAsync(int tagId);
|
||||||
Task<CollectionTag> GetFullTagAsync(int tagId);
|
Task<CollectionTag> GetFullTagAsync(int tagId);
|
||||||
void Update(CollectionTag tag);
|
void Update(CollectionTag tag);
|
||||||
|
Task<int> RemoveTagsWithoutSeries();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
// NOTE: Why do I do this? By the time this method gets executed, the chapter has already been calculated for
|
// NOTE: Why do I do this? By the time this method gets executed, the chapter has already been calculated for
|
||||||
// Plus how can we have a volume without at least 1 chapter?
|
// Plus how can we have a volume without at least 1 chapter?
|
||||||
var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
var firstFile = firstChapter.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||||
if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified))
|
if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified))
|
||||||
{
|
{
|
||||||
firstChapter.CoverImage = GetCoverImage(firstFile);
|
firstChapter.CoverImage = GetCoverImage(firstFile);
|
||||||
|
@ -117,6 +117,7 @@ namespace API.Services
|
|||||||
BackgroundJob.Enqueue(() => _cleanupService.Cleanup());
|
BackgroundJob.Enqueue(() => _cleanupService.Cleanup());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void CleanupChapters(int[] chapterIds)
|
public void CleanupChapters(int[] chapterIds)
|
||||||
{
|
{
|
||||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||||
|
@ -21,6 +21,9 @@ namespace API.Services.Tasks
|
|||||||
_backupService = backupService;
|
_backupService = backupService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up Temp, cache, and old database backups
|
||||||
|
/// </summary>
|
||||||
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
||||||
public void Cleanup()
|
public void Cleanup()
|
||||||
{
|
{
|
||||||
@ -31,7 +34,6 @@ namespace API.Services.Tasks
|
|||||||
_cacheService.Cleanup();
|
_cacheService.Cleanup();
|
||||||
_logger.LogInformation("Cleaning old database backups");
|
_logger.LogInformation("Cleaning old database backups");
|
||||||
_backupService.CleanupBackups();
|
_backupService.CleanupBackups();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,8 @@ namespace API.Services.Tasks
|
|||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
|
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
|
||||||
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
|
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
|
||||||
CleanupUserProgress();
|
|
||||||
|
CleanupDbEntities();
|
||||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate));
|
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate));
|
||||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||||
}
|
}
|
||||||
@ -134,6 +135,11 @@ namespace API.Services.Tasks
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans a library for file changes. If force update passed, all entities will be rechecked for new cover images and comicInfo.xml changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryId"></param>
|
||||||
|
/// <param name="forceUpdate"></param>
|
||||||
[DisableConcurrentExecution(360)]
|
[DisableConcurrentExecution(360)]
|
||||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public void ScanLibrary(int libraryId, bool forceUpdate)
|
public void ScanLibrary(int libraryId, bool forceUpdate)
|
||||||
@ -190,6 +196,16 @@ namespace API.Services.Tasks
|
|||||||
_logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp);
|
_logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up any abandoned rows due to removals from Scan loop
|
||||||
|
/// </summary>
|
||||||
|
private void CleanupDbEntities()
|
||||||
|
{
|
||||||
|
CleanupUserProgress();
|
||||||
|
var cleanedUp = Task.Run( () => _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries()).Result;
|
||||||
|
_logger.LogInformation("Removed {Count} abandoned collection tags", cleanedUp);
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateLibrary(Library library, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
private void UpdateLibrary(Library library, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||||
{
|
{
|
||||||
if (parsedSeries == null) throw new ArgumentNullException(nameof(parsedSeries));
|
if (parsedSeries == null) throw new ArgumentNullException(nameof(parsedSeries));
|
||||||
|
@ -141,8 +141,7 @@ namespace Kavita.Common
|
|||||||
|
|
||||||
public static int GetPort(string filePath)
|
public static int GetPort(string filePath)
|
||||||
{
|
{
|
||||||
Console.WriteLine(GetAppSettingFilename());
|
const int defaultPort = 5000;
|
||||||
const int defaultPort = 5000;
|
|
||||||
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
|
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
|
||||||
{
|
{
|
||||||
return defaultPort;
|
return defaultPort;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# [<img src="/Logo/kavita.svg" width="32" alt="">]() Kavita
|
# [<img src="/Logo/kavita.svg" width="32" alt="">]() Kavita
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|
!
|
||||||
|
|
||||||
Kavita is a fast, feature rich, cross platform reading server. Built with a focus for manga,
|
Kavita is a fast, feature rich, cross platform reading server. Built with a focus for manga,
|
||||||
and the goal of being a full solution for all your reading needs. Setup your own server and share
|
and the goal of being a full solution for all your reading needs. Setup your own server and share
|
||||||
@ -122,3 +122,4 @@ Thank you to [<img src="/Logo/sentry.svg" alt="" width="64">](https://sentry.io/
|
|||||||
|
|
||||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
* Copyright 2020-2021
|
* Copyright 2020-2021
|
||||||
|
|
||||||
|
@ -5,7 +5,10 @@ export interface Chapter {
|
|||||||
range: string;
|
range: string;
|
||||||
number: string;
|
number: string;
|
||||||
files: Array<MangaFile>;
|
files: Array<MangaFile>;
|
||||||
//coverImage: string;
|
/**
|
||||||
|
* This is used in the UI, it is not updated or sent to Backend
|
||||||
|
*/
|
||||||
|
coverImage: string;
|
||||||
coverImageLocked: boolean;
|
coverImageLocked: boolean;
|
||||||
pages: number;
|
pages: number;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
|
@ -104,13 +104,6 @@ export class ActionFactoryService {
|
|||||||
callback: this.dummyCallback,
|
callback: this.dummyCallback,
|
||||||
requiresAdmin: true
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.volumeActions.push({
|
|
||||||
action: Action.Edit,
|
|
||||||
title: 'Edit',
|
|
||||||
callback: this.dummyCallback,
|
|
||||||
requiresAdmin: false
|
|
||||||
});
|
|
||||||
|
|
||||||
this.chapterActions.push({
|
this.chapterActions.push({
|
||||||
action: Action.Edit,
|
action: Action.Edit,
|
||||||
@ -203,6 +196,12 @@ export class ActionFactoryService {
|
|||||||
title: 'Mark as Unread',
|
title: 'Mark as Unread',
|
||||||
callback: this.dummyCallback,
|
callback: this.dummyCallback,
|
||||||
requiresAdmin: false
|
requiresAdmin: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: Action.Edit,
|
||||||
|
title: 'Info',
|
||||||
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<li *ngFor="let member of members" class="list-group-item">
|
<li *ngFor="let member of members" class="list-group-item">
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="(presence.onlineUsers$ | async)?.includes(member.username)"></i>{{member.username | titlecase}} <span *ngIf="member.isAdmin" class="badge badge-pill badge-secondary">Admin</span>
|
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (presence.onlineUsers$ | async)?.includes(member.username)"></i>{{member.username | titlecase}} <span *ngIf="member.isAdmin" class="badge badge-pill badge-secondary">Admin</span>
|
||||||
<div class="float-right" *ngIf="canEditMember(member)">
|
<div class="float-right" *ngIf="canEditMember(member)">
|
||||||
<button class="btn btn-danger mr-2" (click)="deleteUser(member)" placement="top" ngbTooltip="Delete User" attr.aria-label="Delete User {{member.username | titlecase}}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
<button class="btn btn-danger mr-2" (click)="deleteUser(member)" placement="top" ngbTooltip="Delete User" attr.aria-label="Delete User {{member.username | titlecase}}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
||||||
<button class="btn btn-secondary mr-2" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
<button class="btn btn-secondary mr-2" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
||||||
|
@ -30,11 +30,20 @@
|
|||||||
<h4 *ngIf="!utilityService.isChapter(data)">Chapters</h4>
|
<h4 *ngIf="!utilityService.isChapter(data)">Chapters</h4>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li class="media my-4" *ngFor="let chapter of chapters">
|
<li class="media my-4" *ngFor="let chapter of chapters">
|
||||||
<img class="mr-3" style="width: 74px" src="{{imageService.randomize(imageService.getChapterCoverImage(chapter.id))}}">
|
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read Chapter {{chapter.number}}">
|
||||||
|
<img class="mr-3" style="width: 74px" [src]="chapter.coverImage">
|
||||||
|
</a>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<h5 class="mt-0 mb-1">
|
<h5 class="mt-0 mb-1">
|
||||||
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
||||||
Chapter {{formatChapterNumber(chapter)}}
|
<span class="">
|
||||||
|
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="'Chapter' + formatChapterNumber(chapter)"></app-card-actionables>
|
||||||
|
</span> Chapter {{formatChapterNumber(chapter)}}
|
||||||
|
<span class="badge badge-primary badge-pill">
|
||||||
|
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||||
|
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
||||||
|
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<ng-template #specialHeader>File(s)</ng-template>
|
<ng-template #specialHeader>File(s)</ng-template>
|
||||||
</h5>
|
</h5>
|
||||||
@ -57,7 +66,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-info" (click)="updateCover()">Update Cover</button>
|
<button type="button" class="btn btn-info" [disabled]="!isAdmin" (click)="updateCover()">Update Cover</button>
|
||||||
<button type="submit" class="btn btn-primary" (click)="close()">Close</button>
|
<button type="submit" class="btn btn-primary" (click)="close()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
import { Chapter } from 'src/app/_models/chapter';
|
import { Chapter } from 'src/app/_models/chapter';
|
||||||
import { MangaFile } from 'src/app/_models/manga-file';
|
import { MangaFile } from 'src/app/_models/manga-file';
|
||||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||||
import { Volume } from 'src/app/_models/volume';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
|
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||||
|
import { ActionService } from 'src/app/_services/action.service';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
import { UploadService } from 'src/app/_services/upload.service';
|
import { UploadService } from 'src/app/_services/upload.service';
|
||||||
import { ChangeCoverImageModalComponent } from '../change-cover-image/change-cover-image-modal.component';
|
import { ChangeCoverImageModalComponent } from '../change-cover-image/change-cover-image-modal.component';
|
||||||
@ -21,6 +25,7 @@ export class CardDetailsModalComponent implements OnInit {
|
|||||||
|
|
||||||
@Input() parentName = '';
|
@Input() parentName = '';
|
||||||
@Input() seriesId: number = 0;
|
@Input() seriesId: number = 0;
|
||||||
|
@Input() libraryId: number = 0;
|
||||||
@Input() data!: any; // Volume | Chapter
|
@Input() data!: any; // Volume | Chapter
|
||||||
isChapter = false;
|
isChapter = false;
|
||||||
chapters: Chapter[] = [];
|
chapters: Chapter[] = [];
|
||||||
@ -31,20 +36,34 @@ export class CardDetailsModalComponent implements OnInit {
|
|||||||
* If a cover image update occured.
|
* If a cover image update occured.
|
||||||
*/
|
*/
|
||||||
coverImageUpdate: boolean = false;
|
coverImageUpdate: boolean = false;
|
||||||
|
isAdmin: boolean = false;
|
||||||
|
actions: ActionItem<any>[] = [];
|
||||||
|
chapterActions: ActionItem<Chapter>[] = [];
|
||||||
|
|
||||||
|
|
||||||
constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
|
constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
|
||||||
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService) { }
|
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
|
||||||
|
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
|
||||||
|
private actionService: ActionService, private router: Router) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isChapter = this.utilityService.isChapter(this.data);
|
this.isChapter = this.utilityService.isChapter(this.data);
|
||||||
|
|
||||||
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
|
if (user) {
|
||||||
|
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit);
|
||||||
|
|
||||||
if (this.isChapter) {
|
if (this.isChapter) {
|
||||||
this.chapters.push(this.data);
|
this.chapters.push(this.data);
|
||||||
} else if (!this.isChapter) {
|
} else if (!this.isChapter) {
|
||||||
this.chapters.push(...this.data?.chapters);
|
this.chapters.push(...this.data?.chapters);
|
||||||
}
|
}
|
||||||
this.chapters.sort(this.utilityService.sortChapters);
|
this.chapters.sort(this.utilityService.sortChapters);
|
||||||
|
this.chapters.forEach(c => c.coverImage = this.imageService.getChapterCoverImage(c.id));
|
||||||
// Try to show an approximation of the reading order for files
|
// Try to show an approximation of the reading order for files
|
||||||
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||||
this.chapters.forEach((c: Chapter) => {
|
this.chapters.forEach((c: Chapter) => {
|
||||||
@ -63,10 +82,17 @@ export class CardDetailsModalComponent implements OnInit {
|
|||||||
return chapter.number;
|
return chapter.number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
performAction(action: ActionItem<any>, chapter: Chapter) {
|
||||||
|
if (typeof action.callback === 'function') {
|
||||||
|
action.callback(action.action, chapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateCover() {
|
updateCover() {
|
||||||
const modalRef = this.modalService.open(ChangeCoverImageModalComponent, { size: 'lg' }); // scrollable: true, size: 'lg', windowClass: 'scrollable-modal' (these don't work well on mobile)
|
const modalRef = this.modalService.open(ChangeCoverImageModalComponent, { size: 'lg' }); // scrollable: true, size: 'lg', windowClass: 'scrollable-modal' (these don't work well on mobile)
|
||||||
if (this.utilityService.isChapter(this.data)) {
|
if (this.utilityService.isChapter(this.data)) {
|
||||||
const chapter = this.utilityService.asChapter(this.data)
|
const chapter = this.utilityService.asChapter(this.data)
|
||||||
|
chapter.coverImage = this.imageService.getChapterCoverImage(chapter.id);
|
||||||
modalRef.componentInstance.chapter = chapter;
|
modalRef.componentInstance.chapter = chapter;
|
||||||
modalRef.componentInstance.title = 'Select ' + (chapter.isSpecial ? '' : 'Chapter ') + chapter.range + '\'s Cover';
|
modalRef.componentInstance.title = 'Select ' + (chapter.isSpecial ? '' : 'Chapter ') + chapter.range + '\'s Cover';
|
||||||
} else {
|
} else {
|
||||||
@ -85,8 +111,52 @@ export class CardDetailsModalComponent implements OnInit {
|
|||||||
this.uploadService.resetChapterCoverLock(closeResult.chapter.id).subscribe(() => {
|
this.uploadService.resetChapterCoverLock(closeResult.chapter.id).subscribe(() => {
|
||||||
this.toastr.info('Please refresh in a bit for the cover image to be reflected.');
|
this.toastr.info('Please refresh in a bit for the cover image to be reflected.');
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
closeResult.chapter.coverImage = this.imageService.randomize(this.imageService.getChapterCoverImage(closeResult.chapter.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markChapterAsRead(chapter: Chapter) {
|
||||||
|
if (this.seriesId === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionService.markChapterAsRead(this.seriesId, chapter, () => { /* No Action */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
markChapterAsUnread(chapter: Chapter) {
|
||||||
|
if (this.seriesId === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { /* No Action */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChapterActionCallback(action: Action, chapter: Chapter) {
|
||||||
|
switch (action) {
|
||||||
|
case(Action.MarkAsRead):
|
||||||
|
this.markChapterAsRead(chapter);
|
||||||
|
break;
|
||||||
|
case(Action.MarkAsUnread):
|
||||||
|
this.markChapterAsUnread(chapter);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readChapter(chapter: Chapter) {
|
||||||
|
if (chapter.pages === 0) {
|
||||||
|
this.toastr.error('There are no pages. Kavita was not able to read this archive.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chapter.files.length > 0 && chapter.files[0].format === MangaFormat.EPUB) {
|
||||||
|
this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'book', chapter.id]);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,10 +53,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
private uploadService: UploadService) { }
|
private uploadService: UploadService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// this.imageUrls.push({
|
|
||||||
// imageUrl: this.imageService.getSeriesCoverImage(this.series.id),
|
|
||||||
// source: 'Url'
|
|
||||||
// });
|
|
||||||
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
|
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
|
||||||
|
|
||||||
this.libraryService.getLibraryNames().pipe(takeUntil(this.onDestroy)).subscribe(names => {
|
this.libraryService.getLibraryNames().pipe(takeUntil(this.onDestroy)).subscribe(names => {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="overlay" (click)="handleClick()">
|
<div class="overlay" (click)="handleClick()">
|
||||||
<img *ngIf="total > 0 || supressArchiveWarning" class="card-img-top lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
<img *ngIf="total > 0 || supressArchiveWarning" class="img-top lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
||||||
(error)="imageService.updateErroredImage($event)" aria-hidden="true" height="230px" width="158px">
|
(error)="imageService.updateErroredImage($event)" aria-hidden="true" height="230px" width="158px">
|
||||||
<img *ngIf="total === 0 && !supressArchiveWarning" class="card-img-top lazyload" [src]="imageService.errorImage" [attr.data-src]="imageUrl"
|
<img *ngIf="total === 0 && !supressArchiveWarning" class="img-top lazyload" [src]="imageService.errorImage" [attr.data-src]="imageUrl"
|
||||||
aria-hidden="true" height="230px" width="158px">
|
aria-hidden="true" height="230px" width="158px">
|
||||||
<div class="progress-banner" *ngIf="read < total && total > 0 && read !== (total -1)">
|
<div class="progress-banner" *ngIf="read < total && total > 0 && read !== (total -1)">
|
||||||
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
||||||
|
@ -38,7 +38,7 @@ $image-width: 160px;
|
|||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-img-top {
|
.img-top {
|
||||||
height: $image-height;
|
height: $image-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -390,6 +390,7 @@ export class SeriesDetailComponent implements OnInit {
|
|||||||
modalRef.componentInstance.data = data;
|
modalRef.componentInstance.data = data;
|
||||||
modalRef.componentInstance.parentName = this.series?.name;
|
modalRef.componentInstance.parentName = this.series?.name;
|
||||||
modalRef.componentInstance.seriesId = this.series?.id;
|
modalRef.componentInstance.seriesId = this.series?.id;
|
||||||
|
modalRef.componentInstance.libraryId = this.series?.libraryId;
|
||||||
modalRef.closed.subscribe((result: {coverImageUpdate: boolean}) => {
|
modalRef.closed.subscribe((result: {coverImageUpdate: boolean}) => {
|
||||||
if (result.coverImageUpdate) {
|
if (result.coverImageUpdate) {
|
||||||
this.coverImageOffset += 1;
|
this.coverImageOffset += 1;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user