diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 54d304dd6..78b7f23b7 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using API.Constants; using API.DTOs; @@ -17,20 +18,20 @@ namespace API.Controllers private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly ITokenService _tokenService; - private readonly IUserRepository _userRepository; + private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IMapper _mapper; public AccountController(UserManager userManager, SignInManager signInManager, - ITokenService tokenService, IUserRepository userRepository, + ITokenService tokenService, IUnitOfWork unitOfWork, ILogger logger, IMapper mapper) { _userManager = userManager; _signInManager = signInManager; _tokenService = tokenService; - _userRepository = userRepository; + _unitOfWork = unitOfWork; _logger = logger; _mapper = mapper; } @@ -38,7 +39,7 @@ namespace API.Controllers [HttpPost("register")] public async Task> Register(RegisterDto registerDto) { - if (await UserExists(registerDto.Username)) + if (await _userManager.Users.AnyAsync(x => x.UserName == registerDto.Username)) { return BadRequest("Username is taken."); } @@ -54,6 +55,20 @@ namespace API.Controllers if (!roleResult.Succeeded) return BadRequest(result.Errors); + // When we register an admin, we need to grant them access to all Libraries. + if (registerDto.IsAdmin) + { + _logger.LogInformation($"{user.UserName} is being registered as admin. Granting access to all libraries."); + var libraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync(); + foreach (var lib in libraries) + { + lib.AppUsers ??= new List(); + lib.AppUsers.Add(user); + } + } + + if (!await _unitOfWork.Complete()) _logger.LogInformation("There was an issue granting library access. Please do this manually."); + return new UserDto { Username = user.UserName, @@ -76,8 +91,8 @@ namespace API.Controllers // Update LastActive on account user.LastActive = DateTime.Now; - _userRepository.Update(user); - await _userRepository.SaveAllAsync(); + _unitOfWork.UserRepository.Update(user); + await _unitOfWork.Complete(); _logger.LogInformation($"{user.UserName} logged in at {user.LastActive}"); @@ -87,10 +102,5 @@ namespace API.Controllers Token = await _tokenService.CreateToken(user) }; } - - private async Task UserExists(string username) - { - return await _userManager.Users.AnyAsync(user => user.UserName == username.ToLower()); - } } } \ No newline at end of file diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 9cc9202e8..67990db2e 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -7,7 +8,6 @@ using API.Entities; using API.Extensions; using API.Interfaces; using AutoMapper; -using Hangfire; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -18,26 +18,20 @@ namespace API.Controllers public class LibraryController : BaseApiController { private readonly IDirectoryService _directoryService; - private readonly ILibraryRepository _libraryRepository; private readonly ILogger _logger; - private readonly IUserRepository _userRepository; private readonly IMapper _mapper; private readonly ITaskScheduler _taskScheduler; - private readonly ISeriesRepository _seriesRepository; - private readonly ICacheService _cacheService; + private readonly IUnitOfWork _unitOfWork; public LibraryController(IDirectoryService directoryService, - ILibraryRepository libraryRepository, ILogger logger, IUserRepository userRepository, - IMapper mapper, ITaskScheduler taskScheduler, ISeriesRepository seriesRepository, ICacheService cacheService) + ILogger logger, IMapper mapper, ITaskScheduler taskScheduler, + IUnitOfWork unitOfWork) { _directoryService = directoryService; - _libraryRepository = libraryRepository; _logger = logger; - _userRepository = userRepository; _mapper = mapper; _taskScheduler = taskScheduler; - _seriesRepository = seriesRepository; - _cacheService = cacheService; + _unitOfWork = unitOfWork; } /// @@ -49,38 +43,33 @@ namespace API.Controllers [HttpPost("create")] public async Task AddLibrary(CreateLibraryDto createLibraryDto) { - if (await _libraryRepository.LibraryExists(createLibraryDto.Name)) + if (await _unitOfWork.LibraryRepository.LibraryExists(createLibraryDto.Name)) { return BadRequest("Library name already exists. Please choose a unique name to the server."); } - - var admins = (await _userRepository.GetAdminUsersAsync()).ToList(); var library = new Library { Name = createLibraryDto.Name, Type = createLibraryDto.Type, - AppUsers = admins, Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList() }; + _unitOfWork.LibraryRepository.Add(library); + + var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList(); foreach (var admin in admins) { - // If user is null, then set it admin.Libraries ??= new List(); admin.Libraries.Add(library); } - - - if (await _userRepository.SaveAllAsync()) - { - _logger.LogInformation($"Created a new library: {library.Name}"); - var createdLibrary = await _libraryRepository.GetLibraryForNameAsync(library.Name); - BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id, false)); - return Ok(); - } - return BadRequest("There was a critical issue. Please try again."); + + if (!await _unitOfWork.Complete()) return BadRequest("There was a critical issue. Please try again."); + + _logger.LogInformation($"Created a new library: {library.Name}"); + _taskScheduler.ScanLibrary(library.Id); + return Ok(); } /// @@ -105,30 +94,50 @@ namespace API.Controllers [HttpGet] public async Task>> GetLibraries() { - return Ok(await _libraryRepository.GetLibrariesAsync()); + return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()); } [Authorize(Policy = "RequireAdminRole")] - [HttpPut("update-for")] - public async Task> AddLibraryToUser(UpdateLibraryForUserDto updateLibraryForUserDto) + [HttpPost("grant-access")] + public async Task> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto) { - var user = await _userRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username); - + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username); if (user == null) return BadRequest("Could not validate user"); - - user.Libraries = new List(); - - foreach (var selectedLibrary in updateLibraryForUserDto.SelectedLibraries) + + var libraryString = String.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name)); + _logger.LogInformation($"Granting user {updateLibraryForUserDto.Username} access to: {libraryString}"); + + var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync(); + foreach (var library in allLibraries) { - user.Libraries.Add(_mapper.Map(selectedLibrary)); + library.AppUsers ??= new List(); + var libraryContainsUser = library.AppUsers.Any(u => u.UserName == user.UserName); + var libraryIsSelected = updateLibraryForUserDto.SelectedLibraries.Any(l => l.Id == library.Id); + if (libraryContainsUser && !libraryIsSelected) + { + // Remove + library.AppUsers.Remove(user); + } + else if (!libraryContainsUser && libraryIsSelected) + { + library.AppUsers.Add(user); + } + } - if (await _userRepository.SaveAllAsync()) + if (!_unitOfWork.HasChanges()) { _logger.LogInformation($"Added: {updateLibraryForUserDto.SelectedLibraries} to {updateLibraryForUserDto.Username}"); - return Ok(user); + return Ok(_mapper.Map(user)); } + if (await _unitOfWork.Complete()) + { + _logger.LogInformation($"Added: {updateLibraryForUserDto.SelectedLibraries} to {updateLibraryForUserDto.Username}"); + return Ok(_mapper.Map(user)); + } + + return BadRequest("There was a critical issue. Please try again."); } @@ -136,20 +145,21 @@ namespace API.Controllers [HttpPost("scan")] public ActionResult Scan(int libraryId) { - BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(libraryId, true)); + _taskScheduler.ScanLibrary(libraryId, true); return Ok(); } - [HttpGet("libraries-for")] - public async Task>> GetLibrariesForUser(string username) + [HttpGet("libraries")] + public async Task>> GetLibrariesForUser() { - return Ok(await _libraryRepository.GetLibrariesDtoForUsernameAsync(username)); + return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername())); } [HttpGet("series")] public async Task>> GetSeriesForLibrary(int libraryId) { - return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id)); } [Authorize(Policy = "RequireAdminRole")] @@ -158,14 +168,14 @@ namespace API.Controllers { var username = User.GetUsername(); _logger.LogInformation($"Library {libraryId} is being deleted by {username}."); - var series = await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId); - var volumes = (await _seriesRepository.GetVolumesForSeriesAsync(series.Select(x => x.Id).ToArray())) + var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId); + var volumes = (await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(series.Select(x => x.Id).ToArray())) .Select(x => x.Id).ToArray(); - var result = await _libraryRepository.DeleteLibrary(libraryId); + var result = await _unitOfWork.LibraryRepository.DeleteLibrary(libraryId); if (result && volumes.Any()) { - BackgroundJob.Enqueue(() => _cacheService.CleanupVolumes(volumes)); + _taskScheduler.CleanupVolumes(volumes); } return Ok(result); @@ -175,29 +185,24 @@ namespace API.Controllers [HttpPost("update")] public async Task UpdateLibrary(UpdateLibraryDto libraryForUserDto) { - var library = await _libraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id); var originalFolders = library.Folders.Select(x => x.Path); var differenceBetweenFolders = originalFolders.Except(libraryForUserDto.Folders); library.Name = libraryForUserDto.Name; library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList(); - - - - _libraryRepository.Update(library); - if (await _libraryRepository.SaveAllAsync()) + _unitOfWork.LibraryRepository.Update(library); + + if (!await _unitOfWork.Complete()) return BadRequest("There was a critical issue updating the library."); + if (differenceBetweenFolders.Any()) { - if (differenceBetweenFolders.Any()) - { - BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(library.Id, true)); - } - - return Ok(); + _taskScheduler.ScanLibrary(library.Id, true); } - - return BadRequest("There was a critical issue updating the library."); + + return Ok(); + } } } \ No newline at end of file diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 17a5d753d..d2fed05f7 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -1,7 +1,13 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.Data; using API.DTOs; +using API.Entities; +using API.Extensions; using API.Interfaces; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace API.Controllers { @@ -9,11 +15,16 @@ namespace API.Controllers { private readonly IDirectoryService _directoryService; private readonly ICacheService _cacheService; + private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; - public ReaderController(IDirectoryService directoryService, ICacheService cacheService) + public ReaderController(IDirectoryService directoryService, ICacheService cacheService, + ILogger logger, IUnitOfWork unitOfWork) { _directoryService = directoryService; _cacheService = cacheService; + _logger = logger; + _unitOfWork = unitOfWork; } [HttpGet("image")] @@ -28,5 +39,54 @@ namespace API.Controllers return Ok(file); } + + [HttpGet("get-bookmark")] + public async Task> GetBookmark(int volumeId) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + if (user.Progresses == null) return Ok(0); + var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.VolumeId == volumeId); + + if (progress != null) return Ok(progress.PagesRead); + + return Ok(0); + } + + [HttpPost("bookmark")] + public async Task Bookmark(BookmarkDto bookmarkDto) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + _logger.LogInformation($"Saving {user.UserName} progress for {bookmarkDto.VolumeId} to page {bookmarkDto.PageNum}"); + + user.Progresses ??= new List(); + var userProgress = user.Progresses.SingleOrDefault(x => x.VolumeId == bookmarkDto.VolumeId && x.AppUserId == user.Id); + + if (userProgress == null) + { + + user.Progresses.Add(new AppUserProgress + { + PagesRead = bookmarkDto.PageNum, + VolumeId = bookmarkDto.VolumeId, + SeriesId = bookmarkDto.SeriesId, + }); + } + else + { + userProgress.PagesRead = bookmarkDto.PageNum; + userProgress.SeriesId = bookmarkDto.SeriesId; + + } + + _unitOfWork.UserRepository.Update(user); + + if (await _unitOfWork.Complete()) + { + return Ok(); + } + + + return BadRequest("Could not save progress"); + } } } \ No newline at end of file diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 199c5971c..af2d36bb6 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -4,8 +4,6 @@ using System.Threading.Tasks; using API.DTOs; using API.Extensions; using API.Interfaces; -using AutoMapper; -using Hangfire; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -15,26 +13,20 @@ namespace API.Controllers public class SeriesController : BaseApiController { private readonly ILogger _logger; - private readonly IMapper _mapper; private readonly ITaskScheduler _taskScheduler; - private readonly ISeriesRepository _seriesRepository; - private readonly ICacheService _cacheService; + private readonly IUnitOfWork _unitOfWork; - public SeriesController(ILogger logger, IMapper mapper, - ITaskScheduler taskScheduler, ISeriesRepository seriesRepository, - ICacheService cacheService) + public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork) { _logger = logger; - _mapper = mapper; _taskScheduler = taskScheduler; - _seriesRepository = seriesRepository; - _cacheService = cacheService; + _unitOfWork = unitOfWork; } [HttpGet("{seriesId}")] public async Task> GetSeries(int seriesId) { - return Ok(await _seriesRepository.GetSeriesDtoByIdAsync(seriesId)); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId)); } [Authorize(Policy = "RequireAdminRole")] @@ -42,13 +34,13 @@ namespace API.Controllers public async Task> DeleteSeries(int seriesId) { var username = User.GetUsername(); - var volumes = (await _seriesRepository.GetVolumesForSeriesAsync(new []{seriesId})).Select(x => x.Id).ToArray(); + var volumes = (await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(new []{seriesId})).Select(x => x.Id).ToArray(); _logger.LogInformation($"Series {seriesId} is being deleted by {username}."); - var result = await _seriesRepository.DeleteSeriesAsync(seriesId); + var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId); if (result) { - BackgroundJob.Enqueue(() => _cacheService.CleanupVolumes(volumes)); + _taskScheduler.CleanupVolumes(volumes); } return Ok(result); } @@ -56,13 +48,14 @@ namespace API.Controllers [HttpGet("volumes")] public async Task>> GetVolumes(int seriesId) { - return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId)); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id)); } [HttpGet("volume")] public async Task> GetVolume(int volumeId) { - return Ok(await _seriesRepository.GetVolumeDtoAsync(volumeId)); + return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId)); } } } \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 535345b75..6a98eedd9 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -12,27 +12,22 @@ namespace API.Controllers [Authorize] public class UsersController : BaseApiController { - private readonly IUserRepository _userRepository; - private readonly ILibraryRepository _libraryRepository; + private readonly IUnitOfWork _unitOfWork; - public UsersController(IUserRepository userRepository, ILibraryRepository libraryRepository) + public UsersController(IUnitOfWork unitOfWork) { - _userRepository = userRepository; - _libraryRepository = libraryRepository; + _unitOfWork = unitOfWork; } [Authorize(Policy = "RequireAdminRole")] [HttpDelete("delete-user")] public async Task DeleteUser(string username) { - var user = await _userRepository.GetUserByUsernameAsync(username); - _userRepository.Delete(user); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username); + _unitOfWork.UserRepository.Delete(user); + + if (await _unitOfWork.Complete()) return Ok(); - if (await _userRepository.SaveAllAsync()) - { - return Ok(); - } - return BadRequest("Could not delete the user."); } @@ -40,18 +35,13 @@ namespace API.Controllers [HttpGet] public async Task>> GetUsers() { - return Ok(await _userRepository.GetMembersAsync()); + return Ok(await _unitOfWork.UserRepository.GetMembersAsync()); } [HttpGet("has-library-access")] public async Task> HasLibraryAccess(int libraryId) { - var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); - - if (user == null) return BadRequest("Could not validate user"); - - var libs = await _libraryRepository.GetLibrariesDtoForUsernameAsync(user.UserName); - + var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()); return Ok(libs.Any(x => x.Id == libraryId)); } } diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index eb10f3d0d..50160ab5c 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -2,11 +2,16 @@ { public class SeriesDto { - public int Id { get; set; } - public string Name { get; set; } - public string OriginalName { get; set; } - public string SortName { get; set; } - public string Summary { get; set; } - public byte[] CoverImage { get; set; } + public int Id { get; init; } + public string Name { get; init; } + public string OriginalName { get; init; } + public string SortName { get; init; } + public string Summary { get; init; } + public byte[] CoverImage { get; init; } + public int Pages { get; init; } + /// + /// Sum of pages read from linked Volumes. Calculated at API-time. + /// + public int PagesRead { get; set; } } } \ No newline at end of file diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 33ed702c8..a57465857 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -8,5 +8,6 @@ namespace API.DTOs public string Name { get; set; } public byte[] CoverImage { get; set; } public int Pages { get; set; } + public int PagesRead { get; set; } } } \ No newline at end of file diff --git a/API/Data/BookmarkDto.cs b/API/Data/BookmarkDto.cs new file mode 100644 index 000000000..ea6654165 --- /dev/null +++ b/API/Data/BookmarkDto.cs @@ -0,0 +1,9 @@ +namespace API.Data +{ + public class BookmarkDto + { + public int VolumeId { get; init; } + public int PageNum { get; init; } + public int SeriesId { get; init; } + } +} \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 7a75ad138..2bb9424c0 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -21,7 +21,8 @@ namespace API.Data public DbSet Library { get; set; } public DbSet Series { get; set; } public DbSet Volume { get; set; } - + public DbSet AppUser { get; set; } + public DbSet AppUserProgresses { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index 15cd38d8b..a85435df2 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using API.DTOs; @@ -20,29 +22,34 @@ namespace API.Data _context = context; _mapper = mapper; } + + public void Add(Library library) + { + _context.Library.Add(library); + } public void Update(Library library) { _context.Entry(library).State = EntityState.Modified; } - public async Task SaveAllAsync() + public async Task> GetLibraryDtosForUsernameAsync(string userName) { - return await _context.SaveChangesAsync() > 0; - } - - public bool SaveAll() - { - return _context.SaveChanges() > 0; - } - - public async Task> GetLibrariesDtoForUsernameAsync(string userName) - { - // TODO: Speed this query up - return await _context.Library + Stopwatch sw = Stopwatch.StartNew(); + var libs = await _context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(x => x.UserName == userName)) .ProjectTo(_mapper.ConfigurationProvider) + .AsNoTracking() + .ToListAsync(); + Console.WriteLine("Processed GetLibraryDtosForUsernameAsync in {0} milliseconds", sw.ElapsedMilliseconds); + return libs; + } + + public async Task> GetLibrariesAsync() + { + return await _context.Library + .Include(l => l.AppUsers) .ToListAsync(); } @@ -62,7 +69,7 @@ namespace API.Data return await _context.SaveChangesAsync() > 0; } - public async Task> GetLibrariesAsync() + public async Task> GetLibraryDtosAsync() { return await _context.Library .Include(f => f.Folders) @@ -74,6 +81,7 @@ namespace API.Data return await _context.Library .Where(x => x.Id == libraryId) .Include(f => f.Folders) + .Include(l => l.Series) .SingleAsync(); } diff --git a/API/Data/Migrations/20210114214506_UserProgress.Designer.cs b/API/Data/Migrations/20210114214506_UserProgress.Designer.cs new file mode 100644 index 000000000..cd7e5a53b --- /dev/null +++ b/API/Data/Migrations/20210114214506_UserProgress.Designer.cs @@ -0,0 +1,576 @@ +// +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("20210114214506_UserProgress")] + partial class UserProgress + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("VolumeId"); + + b.ToTable("AppUserProgress"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Chapter") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("NumberOfPages") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("ProgressId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProgressId"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", null) + .WithMany("Progresses") + .HasForeignKey("VolumeId"); + + 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.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.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + 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.Volume", b => + { + b.HasOne("API.Entities.AppUserProgress", "Progress") + .WithMany() + .HasForeignKey("ProgressId"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Progress"); + + 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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + + b.Navigation("Progresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210114214506_UserProgress.cs b/API/Data/Migrations/20210114214506_UserProgress.cs new file mode 100644 index 000000000..6d966fbdc --- /dev/null +++ b/API/Data/Migrations/20210114214506_UserProgress.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class UserProgress : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ProgressId", + table: "Volume", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateTable( + name: "AppUserProgress", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PagesRead = table.Column(type: "INTEGER", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false), + VolumeId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserProgress", x => x.Id); + table.ForeignKey( + name: "FK_AppUserProgress_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AppUserProgress_Volume_VolumeId", + column: x => x.VolumeId, + principalTable: "Volume", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Volume_ProgressId", + table: "Volume", + column: "ProgressId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserProgress_AppUserId", + table: "AppUserProgress", + column: "AppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserProgress_VolumeId", + table: "AppUserProgress", + column: "VolumeId"); + + migrationBuilder.AddForeignKey( + name: "FK_Volume_AppUserProgress_ProgressId", + table: "Volume", + column: "ProgressId", + principalTable: "AppUserProgress", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Volume_AppUserProgress_ProgressId", + table: "Volume"); + + migrationBuilder.DropTable( + name: "AppUserProgress"); + + migrationBuilder.DropIndex( + name: "IX_Volume_ProgressId", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "ProgressId", + table: "Volume"); + } + } +} diff --git a/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs b/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs new file mode 100644 index 000000000..d4133c335 --- /dev/null +++ b/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs @@ -0,0 +1,562 @@ +// +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("20210117180406_ReadStatusModifications")] + partial class ReadStatusModifications + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Chapter") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("NumberOfPages") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .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.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.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + 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.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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Progresses"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210117180406_ReadStatusModifications.cs b/API/Data/Migrations/20210117180406_ReadStatusModifications.cs new file mode 100644 index 000000000..d852d8843 --- /dev/null +++ b/API/Data/Migrations/20210117180406_ReadStatusModifications.cs @@ -0,0 +1,154 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class ReadStatusModifications : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserProgress_AspNetUsers_AppUserId", + table: "AppUserProgress"); + + migrationBuilder.DropForeignKey( + name: "FK_AppUserProgress_Volume_VolumeId", + table: "AppUserProgress"); + + migrationBuilder.DropForeignKey( + name: "FK_Volume_AppUserProgress_ProgressId", + table: "Volume"); + + migrationBuilder.DropIndex( + name: "IX_Volume_ProgressId", + table: "Volume"); + + migrationBuilder.DropPrimaryKey( + name: "PK_AppUserProgress", + table: "AppUserProgress"); + + migrationBuilder.DropIndex( + name: "IX_AppUserProgress_VolumeId", + table: "AppUserProgress"); + + migrationBuilder.DropColumn( + name: "ProgressId", + table: "Volume"); + + migrationBuilder.RenameTable( + name: "AppUserProgress", + newName: "AppUserProgresses"); + + migrationBuilder.RenameIndex( + name: "IX_AppUserProgress_AppUserId", + table: "AppUserProgresses", + newName: "IX_AppUserProgresses_AppUserId"); + + migrationBuilder.AlterColumn( + name: "VolumeId", + table: "AppUserProgresses", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "SeriesId", + table: "AppUserProgresses", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddPrimaryKey( + name: "PK_AppUserProgresses", + table: "AppUserProgresses", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserProgresses_AspNetUsers_AppUserId", + table: "AppUserProgresses", + column: "AppUserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserProgresses_AspNetUsers_AppUserId", + table: "AppUserProgresses"); + + migrationBuilder.DropPrimaryKey( + name: "PK_AppUserProgresses", + table: "AppUserProgresses"); + + migrationBuilder.DropColumn( + name: "SeriesId", + table: "AppUserProgresses"); + + migrationBuilder.RenameTable( + name: "AppUserProgresses", + newName: "AppUserProgress"); + + migrationBuilder.RenameIndex( + name: "IX_AppUserProgresses_AppUserId", + table: "AppUserProgress", + newName: "IX_AppUserProgress_AppUserId"); + + migrationBuilder.AddColumn( + name: "ProgressId", + table: "Volume", + type: "INTEGER", + nullable: true); + + migrationBuilder.AlterColumn( + name: "VolumeId", + table: "AppUserProgress", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddPrimaryKey( + name: "PK_AppUserProgress", + table: "AppUserProgress", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_Volume_ProgressId", + table: "Volume", + column: "ProgressId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserProgress_VolumeId", + table: "AppUserProgress", + column: "VolumeId"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserProgress_AspNetUsers_AppUserId", + table: "AppUserProgress", + column: "AppUserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserProgress_Volume_VolumeId", + table: "AppUserProgress", + column: "VolumeId", + principalTable: "Volume", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Volume_AppUserProgress_ProgressId", + table: "Volume", + column: "ProgressId", + principalTable: "AppUserProgress", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs b/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs new file mode 100644 index 000000000..8caa3acc1 --- /dev/null +++ b/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs @@ -0,0 +1,565 @@ +// +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("20210117181421_SeriesPages")] + partial class SeriesPages + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Chapter") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("NumberOfPages") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .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.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.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + 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.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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Progresses"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210117181421_SeriesPages.cs b/API/Data/Migrations/20210117181421_SeriesPages.cs new file mode 100644 index 000000000..97ee23b1b --- /dev/null +++ b/API/Data/Migrations/20210117181421_SeriesPages.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class SeriesPages : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Pages", + table: "Series", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Pages", + table: "Series"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index aa9889bcc..7d345a870 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -118,6 +118,31 @@ namespace API.Data.Migrations b.ToTable("AspNetUsers"); }); + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + modelBuilder.Entity("API.Entities.AppUserRole", b => { b.Property("UserId") @@ -230,6 +255,9 @@ namespace API.Data.Migrations b.Property("OriginalName") .HasColumnType("TEXT"); + b.Property("Pages") + .HasColumnType("INTEGER"); + b.Property("SortName") .HasColumnType("TEXT"); @@ -376,6 +404,17 @@ namespace API.Data.Migrations b.ToTable("AspNetUserTokens"); }); + 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.AppUserRole", b => { b.HasOne("API.Entities.AppRole", "Role") @@ -497,6 +536,8 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.AppUser", b => { + b.Navigation("Progresses"); + b.Navigation("UserRoles"); }); diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index 932b5ca37..510a21388 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using API.DTOs; @@ -21,6 +23,11 @@ namespace API.Data _mapper = mapper; } + public void Add(Series series) + { + _context.Series.Add(series); + } + public void Update(Series series) { _context.Entry(series).State = EntityState.Modified; @@ -46,20 +53,59 @@ namespace API.Data return _context.Series.SingleOrDefault(x => x.Name == name); } - public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId) + public async Task> GetSeriesForLibraryIdAsync(int libraryId) { return await _context.Series - .Where(series => series.LibraryId == libraryId) + .Where(s => s.LibraryId == libraryId) .OrderBy(s => s.SortName) - .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + .ToListAsync(); + } + + public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId) + { + var sw = Stopwatch.StartNew(); + var series = await _context.Series + .Where(s => s.LibraryId == libraryId) + .OrderBy(s => s.SortName) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + + if (userId > 0) + { + var userProgress = await _context.AppUserProgresses + .Where(p => p.AppUserId == userId && series.Select(s => s.Id).Contains(p.SeriesId)) + .ToListAsync(); + + foreach (var s in series) + { + s.PagesRead = userProgress.Where(p => p.SeriesId == s.Id).Sum(p => p.PagesRead); + } + } + + Console.WriteLine("Processed GetSeriesDtoForLibraryIdAsync in {0} milliseconds", sw.ElapsedMilliseconds); + return series; } - public async Task> GetVolumesDtoAsync(int seriesId) + public async Task> GetVolumesDtoAsync(int seriesId, int userId) { - return await _context.Volume + var volumes = await _context.Volume .Where(vol => vol.SeriesId == seriesId) .OrderBy(volume => volume.Number) - .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + .ProjectTo(_mapper.ConfigurationProvider) + .AsNoTracking() + .ToListAsync(); + var userProgress = await _context.AppUserProgresses + .Where(p => p.AppUserId == userId && volumes.Select(s => s.Id).Contains(p.VolumeId)) + .AsNoTracking() + .ToListAsync(); + + foreach (var v in volumes) + { + v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead); + } + + return volumes; + } public IEnumerable GetVolumes(int seriesId) @@ -112,5 +158,10 @@ namespace API.Data return await _context.SaveChangesAsync() > 0; } + + public async Task GetVolumeByIdAsync(int volumeId) + { + return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId); + } } } \ No newline at end of file diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs new file mode 100644 index 000000000..25d0002c7 --- /dev/null +++ b/API/Data/UnitOfWork.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using API.Entities; +using API.Interfaces; +using AutoMapper; +using Microsoft.AspNetCore.Identity; + +namespace API.Data +{ + public class UnitOfWork : IUnitOfWork + { + private readonly DataContext _context; + private readonly IMapper _mapper; + private readonly UserManager _userManager; + + public UnitOfWork(DataContext context, IMapper mapper, UserManager userManager) + { + _context = context; + _mapper = mapper; + _userManager = userManager; + } + + public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper); + public IUserRepository UserRepository => new UserRepository(_context, _userManager); + public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper); + + public async Task Complete() + { + return await _context.SaveChangesAsync() > 0; + } + + public bool HasChanges() + { + return _context.ChangeTracker.HasChanges(); + } + } +} \ No newline at end of file diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 5098b156c..2569e1604 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -5,8 +5,6 @@ using API.Constants; using API.DTOs; using API.Entities; using API.Interfaces; -using AutoMapper; -using AutoMapper.QueryableExtensions; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -15,13 +13,11 @@ namespace API.Data public class UserRepository : IUserRepository { private readonly DataContext _context; - private readonly IMapper _mapper; private readonly UserManager _userManager; - public UserRepository(DataContext context, IMapper mapper, UserManager userManager) + public UserRepository(DataContext context, UserManager userManager) { _context = context; - _mapper = mapper; _userManager = userManager; } @@ -32,27 +28,18 @@ namespace API.Data public void Delete(AppUser user) { - _context.Users.Remove(user); - } - - public async Task SaveAllAsync() - { - return await _context.SaveChangesAsync() > 0; - } - - public async Task> GetUsersAsync() - { - return await _context.Users.ToListAsync(); - } - - public async Task GetUserByIdAsync(int id) - { - return await _context.Users.FindAsync(id); + _context.AppUser.Remove(user); } + /// + /// Gets an AppUser by username. Returns back Progress information. + /// + /// + /// public async Task GetUserByUsernameAsync(string username) { return await _context.Users + .Include(u => u.Progresses) .SingleOrDefaultAsync(x => x.UserName == username); } @@ -63,7 +50,7 @@ namespace API.Data public async Task> GetMembersAsync() { - return await _userManager.Users + return await _context.Users .Include(x => x.Libraries) .Include(r => r.UserRoles) .ThenInclude(r => r.Role) @@ -83,16 +70,8 @@ namespace API.Data Folders = l.Folders.Select(x => x.Path).ToList() }).ToList() }) + .AsNoTracking() .ToListAsync(); } - - public async Task GetMemberAsync(string username) - { - return await _context.Users.Where(x => x.UserName == username) - .Include(x => x.Libraries) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); - } - } } \ No newline at end of file diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 9e66a7d00..10bcec503 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -12,12 +12,11 @@ namespace API.Entities public DateTime Created { get; set; } = DateTime.Now; public DateTime LastActive { get; set; } public ICollection Libraries { get; set; } + public ICollection UserRoles { get; set; } + public ICollection Progresses { get; set; } + [ConcurrencyCheck] public uint RowVersion { get; set; } - - public ICollection UserRoles { get; set; } - - //public ICollection SeriesProgresses { get; set; } public void OnSavingChanges() { diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index cfff3d9d4..0f05f4dee 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -1,10 +1,18 @@ -namespace API.Entities + +namespace API.Entities { /// /// Represents the progress a single user has on a given Volume. /// public class AppUserProgress { + public int Id { get; set; } + public int PagesRead { get; set; } + public int VolumeId { get; set; } + public int SeriesId { get; set; } + // Relationships + public AppUser AppUser { get; set; } + public int AppUserId { get; set; } } } \ No newline at end of file diff --git a/API/Entities/LibraryType.cs b/API/Entities/LibraryType.cs index 6136042b0..6061e3e8f 100644 --- a/API/Entities/LibraryType.cs +++ b/API/Entities/LibraryType.cs @@ -9,8 +9,6 @@ namespace API.Entities [Description("Comic")] Comic = 1, [Description("Book")] - Book = 2, - [Description("Raw")] - Raw = 3 + Book = 2 } } \ No newline at end of file diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index b93c128a3..4c0c675de 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -4,9 +4,12 @@ namespace API.Entities public class MangaFile { public int Id { get; set; } + /// + /// Absolute path to the archive file + /// public string FilePath { get; set; } /// - /// Do not expect this to be set. If this MangaFile represents a volume file, this will be null. + /// Used to track if multiple MangaFiles (archives) represent a single Volume. If only one volume file, this will be 0. /// public int Chapter { get; set; } /// diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 368a04e50..8fe8e6628 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -26,6 +26,12 @@ namespace API.Entities public DateTime Created { get; set; } public DateTime LastModified { get; set; } public byte[] CoverImage { get; set; } + /// + /// Sum of all Volume pages + /// + public int Pages { get; set; } + + // Relationships public ICollection Volumes { get; set; } public Library Library { get; set; } public int LibraryId { get; set; } diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 1550ab335..304c2bfae 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -15,10 +15,9 @@ namespace API.Entities public byte[] CoverImage { get; set; } public int Pages { get; set; } - // public string CachePath {get; set;} // Path where cache is located. Default null, resets to null on deletion. - //public ICollection AppUserProgress { get; set; } + - // Many-to-One relationships + // Relationships public Series Series { get; set; } public int SeriesId { get; set; } } diff --git a/API/Errors/ApiException.cs b/API/Errors/ApiException.cs index 3f026bb64..ce1792f72 100644 --- a/API/Errors/ApiException.cs +++ b/API/Errors/ApiException.cs @@ -2,9 +2,9 @@ { public class ApiException { - public int Status { get; set; } - public string Message { get; set; } - public string Details { get; set; } + public int Status { get; init; } + public string Message { get; init; } + public string Details { get; init; } public ApiException(int status, string message = null, string details = null) { diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 63d6a04a7..fba75e148 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -17,14 +17,13 @@ namespace API.Extensions { services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - + services.AddScoped(); + + + services.AddDbContext(options => { options.UseSqlite(config.GetConnectionString("DefaultConnection")); diff --git a/API/Interfaces/IDirectoryService.cs b/API/Interfaces/IDirectoryService.cs index 9592292c4..93e7c1e7b 100644 --- a/API/Interfaces/IDirectoryService.cs +++ b/API/Interfaces/IDirectoryService.cs @@ -13,14 +13,6 @@ namespace API.Interfaces /// List of folder names IEnumerable ListDirectory(string rootPath); - /// - /// Lists out top-level files for a given directory. - /// TODO: Implement ability to provide a filter for file types (done in another implementation on DirectoryService) - /// - /// Absolute path - /// List of folder names - IList ListFiles(string rootPath); - /// /// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite /// cover images if forceUpdate is true. @@ -29,6 +21,8 @@ namespace API.Interfaces /// Force overwriting for cover images void ScanLibrary(int libraryId, bool forceUpdate); + void ScanLibraries(); + /// /// Returns the path a volume would be extracted to. /// Deprecated. diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 5a919770c..1a0d3f778 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -7,15 +7,14 @@ namespace API.Interfaces { public interface ILibraryRepository { + void Add(Library library); void Update(Library library); - Task SaveAllAsync(); - Task> GetLibrariesAsync(); + Task> GetLibraryDtosAsync(); Task LibraryExists(string libraryName); Task GetLibraryForIdAsync(int libraryId); - bool SaveAll(); - Task> GetLibrariesDtoForUsernameAsync(string userName); + Task> GetLibraryDtosForUsernameAsync(string userName); + Task> GetLibrariesAsync(); Task GetLibraryForNameAsync(string libraryName); - Task DeleteLibrary(int libraryId); } } \ No newline at end of file diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 490628163..a33fc18aa 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -7,13 +7,13 @@ namespace API.Interfaces { public interface ISeriesRepository { + void Add(Series series); void Update(Series series); - Task SaveAllAsync(); Task GetSeriesByNameAsync(string name); Series GetSeriesByName(string name); - bool SaveAll(); - Task> GetSeriesDtoForLibraryIdAsync(int libraryId); - Task> GetVolumesDtoAsync(int seriesId); + Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId); + Task> GetSeriesForLibraryIdAsync(int libraryId); + Task> GetVolumesDtoAsync(int seriesId, int userId); IEnumerable GetVolumes(int seriesId); Task GetSeriesDtoByIdAsync(int seriesId); @@ -22,5 +22,7 @@ namespace API.Interfaces Task> GetVolumesForSeriesAsync(int[] seriesIds); Task DeleteSeriesAsync(int seriesId); + Task GetVolumeByIdAsync(int volumeId); + } } \ No newline at end of file diff --git a/API/Interfaces/ITaskScheduler.cs b/API/Interfaces/ITaskScheduler.cs index 7f0a6312b..7f0370a9a 100644 --- a/API/Interfaces/ITaskScheduler.cs +++ b/API/Interfaces/ITaskScheduler.cs @@ -2,6 +2,8 @@ { public interface ITaskScheduler { - + public void ScanLibrary(int libraryId, bool forceUpdate = false); + + public void CleanupVolumes(int[] volumeIds); } } \ No newline at end of file diff --git a/API/Interfaces/IUnitOfWork.cs b/API/Interfaces/IUnitOfWork.cs new file mode 100644 index 000000000..3b1cf4347 --- /dev/null +++ b/API/Interfaces/IUnitOfWork.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace API.Interfaces +{ + public interface IUnitOfWork + { + ISeriesRepository SeriesRepository { get; } + IUserRepository UserRepository { get; } + ILibraryRepository LibraryRepository { get; } + Task Complete(); + bool HasChanges(); + } +} \ No newline at end of file diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 26e3b5a11..01127050a 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -8,13 +8,9 @@ namespace API.Interfaces public interface IUserRepository { void Update(AppUser user); - Task SaveAllAsync(); - Task> GetUsersAsync(); - Task GetUserByIdAsync(int id); + public void Delete(AppUser user); Task GetUserByUsernameAsync(string username); Task> GetMembersAsync(); - Task GetMemberAsync(string username); - public void Delete(AppUser user); Task> GetAdminUsersAsync(); } } \ No newline at end of file diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index a6372bb1f..ae92e3261 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -13,16 +13,16 @@ namespace API.Services public class CacheService : ICacheService { private readonly IDirectoryService _directoryService; - private readonly ISeriesRepository _seriesRepository; private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; private readonly NumericComparer _numericComparer; private readonly string _cacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../cache/")); - public CacheService(IDirectoryService directoryService, ISeriesRepository seriesRepository, ILogger logger) + public CacheService(IDirectoryService directoryService, ILogger logger, IUnitOfWork unitOfWork) { _directoryService = directoryService; - _seriesRepository = seriesRepository; _logger = logger; + _unitOfWork = unitOfWork; _numericComparer = new NumericComparer(); } @@ -38,7 +38,7 @@ namespace API.Services { return null; } - Volume volume = await _seriesRepository.GetVolumeAsync(volumeId); + Volume volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId); foreach (var file in volume.Files) { var extractPath = GetVolumeCachePath(volumeId, file); @@ -109,12 +109,10 @@ namespace API.Services if (page + 1 < (mangaFile.NumberOfPages + pagesSoFar)) { var path = GetVolumeCachePath(volume.Id, mangaFile); + var files = DirectoryService.GetFiles(path); + Array.Sort(files, _numericComparer); - var files = _directoryService.ListFiles(path); - var array = files.ToArray(); - Array.Sort(array, _numericComparer); // TODO: Find a way to apply numericComparer to IList. - - return array.ElementAt((page + 1) - pagesSoFar); + return files.ElementAt(page - pagesSoFar); } pagesSoFar += mangaFile.NumberOfPages; diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 02358862c..9c619408a 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -23,38 +23,41 @@ namespace API.Services public class DirectoryService : IDirectoryService { private readonly ILogger _logger; - private readonly ISeriesRepository _seriesRepository; - private readonly ILibraryRepository _libraryRepository; + private readonly IUnitOfWork _unitOfWork; private ConcurrentDictionary> _scannedSeries; - public DirectoryService(ILogger logger, - ISeriesRepository seriesRepository, - ILibraryRepository libraryRepository) + public DirectoryService(ILogger logger, IUnitOfWork unitOfWork) { _logger = logger; - _seriesRepository = seriesRepository; - _libraryRepository = libraryRepository; + _unitOfWork = unitOfWork; } /// /// Given a set of regex search criteria, get files in the given path. /// /// Directory to search - /// Regex version of search pattern (ie \.mp3|\.mp4) + /// Regex version of search pattern (ie \.mp3|\.mp4). Defaults to * meaning all files. /// SearchOption to use, defaults to TopDirectoryOnly /// List of file paths - private static IEnumerable GetFiles(string path, + private static IEnumerable GetFilesWithCertainExtensions(string path, string searchPatternExpression = "", SearchOption searchOption = SearchOption.TopDirectoryOnly) { + if (!Directory.Exists(path)) return ImmutableList.Empty; var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase); return Directory.EnumerateFiles(path, "*", searchOption) .Where(file => reSearchPattern.IsMatch(Path.GetExtension(file))); } + + public static string[] GetFiles(string path) + { + if (!Directory.Exists(path)) return Array.Empty(); + return Directory.GetFiles(path); + } - public IEnumerable ListDirectory(string rootPath) + public IEnumerable ListDirectory(string rootPath) { if (!Directory.Exists(rootPath)) return ImmutableList.Empty; @@ -67,13 +70,6 @@ namespace API.Services return dirs; } - public IList ListFiles(string rootPath) - { - if (!Directory.Exists(rootPath)) return ImmutableList.Empty; - return Directory.GetFiles(rootPath); - } - - /// /// Processes files found during a library scan. Generates a collection of series->volume->files for DB processing later. /// @@ -112,19 +108,21 @@ namespace API.Services } } - private Series UpdateSeries(string seriesName, ParserInfo[] infos, bool forceUpdate) + private Series UpdateSeries(Series series, ParserInfo[] infos, bool forceUpdate) { - var series = _seriesRepository.GetSeriesByName(seriesName) ?? new Series - { - Name = seriesName, - OriginalName = seriesName, - SortName = seriesName, - Summary = "" // TODO: Check if comicInfo.xml in file and parse metadata out. - }; - var volumes = UpdateVolumes(series, infos, forceUpdate); series.Volumes = volumes; - series.CoverImage = volumes.OrderBy(x => x.Number).FirstOrDefault()?.CoverImage; + series.Pages = volumes.Sum(v => v.Pages); + if (series.CoverImage == null || forceUpdate) + { + series.CoverImage = volumes.OrderBy(x => x.Number).FirstOrDefault()?.CoverImage; + } + if (string.IsNullOrEmpty(series.Summary) || forceUpdate) + { + series.Summary = ""; // TODO: Check if comicInfo.xml in file and parse metadata out. + } + + return series; } @@ -152,7 +150,7 @@ namespace API.Services private ICollection UpdateVolumes(Series series, ParserInfo[] infos, bool forceUpdate) { ICollection volumes = new List(); - IList existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList(); + IList existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList(); foreach (var info in infos) { @@ -212,13 +210,22 @@ namespace API.Services return volumes; } - public void ScanLibrary(int libraryId, bool forceUpdate) + public void ScanLibraries() + { + var libraries = Task.Run(() => _unitOfWork.LibraryRepository.GetLibrariesAsync()).Result.ToList(); + foreach (var lib in libraries) + { + ScanLibrary(lib.Id, false); + } + } + + public void ScanLibrary(int libraryId, bool forceUpdate) { var sw = Stopwatch.StartNew(); Library library; try { - library = Task.Run(() => _libraryRepository.GetLibraryForIdAsync(libraryId)).Result; + library = Task.Run(() => _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId)).Result; } catch (Exception ex) { @@ -255,19 +262,35 @@ namespace API.Services var series = filtered.ToImmutableDictionary(v => v.Key, v => v.Value); // Perform DB activities - library.Series = new List(); // Temp delete everything until we can mark items Unavailable + var allSeries = Task.Run(() => _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId)).Result.ToList(); foreach (var seriesKey in series.Keys) { - var mangaSeries = UpdateSeries(seriesKey, series[seriesKey].ToArray(), forceUpdate); - _logger.LogInformation($"Created/Updated series {mangaSeries.Name}"); + var mangaSeries = allSeries.SingleOrDefault(s => s.Name == seriesKey) ?? new Series + { + Name = seriesKey, + OriginalName = seriesKey, + SortName = seriesKey, + Summary = "" + }; + mangaSeries = UpdateSeries(mangaSeries, series[seriesKey].ToArray(), forceUpdate); + _logger.LogInformation($"Created/Updated series {mangaSeries.Name} for {library.Name} library"); + library.Series ??= new List(); library.Series.Add(mangaSeries); } - - - _libraryRepository.Update(library); - - if (_libraryRepository.SaveAll()) + // Remove series that are no longer on disk + foreach (var existingSeries in allSeries) + { + if (!series.ContainsKey(existingSeries.Name) || !series.ContainsKey(existingSeries.OriginalName)) + { + // Delete series, there is no file to backup any longer. + library.Series.Remove(existingSeries); + } + } + + _unitOfWork.LibraryRepository.Update(library); + + if (Task.Run(() => _unitOfWork.Complete()).Result) { _logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series."); } @@ -285,46 +308,6 @@ namespace API.Services return Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/"); } - /// - /// TODO: Delete this method - /// - /// - /// - /// - private string ExtractArchive(string archivePath, int volumeId) - { - if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath)) - { - _logger.LogError($"Archive {archivePath} could not be found."); - return ""; - } - - var extractPath = GetExtractPath(volumeId); - - if (Directory.Exists(extractPath)) - { - _logger.LogInformation($"Archive {archivePath} has already been extracted. Returning existing folder."); - return extractPath; - } - - using ZipArchive archive = ZipFile.OpenRead(archivePath); - - // TODO: Throw error if we couldn't extract - var needsFlattening = archive.Entries.Count > 0 && !Path.HasExtension(archive.Entries.ElementAt(0).FullName); - if (!archive.HasFiles() && !needsFlattening) return ""; - - archive.ExtractToDirectory(extractPath); - _logger.LogInformation($"Extracting archive to {extractPath}"); - - if (needsFlattening) - { - _logger.LogInformation("Extracted archive is nested in root folder, flattening..."); - new DirectoryInfo(extractPath).Flatten(); - } - - return extractPath; - } - public string ExtractArchive(string archivePath, string extractPath) { if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath)) @@ -426,7 +409,7 @@ namespace API.Services } try { - files = DirectoryService.GetFiles(currentDir, Parser.Parser.MangaFileExtensions) + files = DirectoryService.GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions) .ToArray(); } catch (UnauthorizedAccessException e) { diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 00fd5597e..490f8b2b3 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -6,18 +6,33 @@ namespace API.Services { public class TaskScheduler : ITaskScheduler { + private readonly ICacheService _cacheService; private readonly ILogger _logger; - private readonly BackgroundJobServer _client; + private readonly IDirectoryService _directoryService; + public BackgroundJobServer Client => new BackgroundJobServer(); - public TaskScheduler(ICacheService cacheService, ILogger logger) + public TaskScheduler(ICacheService cacheService, ILogger logger, + IDirectoryService directoryService) { + _cacheService = cacheService; _logger = logger; - _client = new BackgroundJobServer(); - + _directoryService = directoryService; + _logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis."); - RecurringJob.AddOrUpdate(() => cacheService.Cleanup(), Cron.Daily); + RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily); + RecurringJob.AddOrUpdate(() => directoryService.ScanLibraries(), Cron.Daily); + } + + public void ScanLibrary(int libraryId, bool forceUpdate = false) + { + _logger.LogInformation($"Enqueuing library scan for: {libraryId}"); + BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(libraryId, forceUpdate)); + } + + public void CleanupVolumes(int[] volumeIds) + { + BackgroundJob.Enqueue(() => _cacheService.CleanupVolumes(volumeIds)); + } - - } } \ No newline at end of file