From effdf07cefc563a028af03a25891d2201a364e4e Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 17 Jan 2021 15:05:27 -0600 Subject: [PATCH 01/11] Very messy code that implements read status tracking. Needs major cleanup. --- API/Controllers/AccountController.cs | 11 +- API/Controllers/LibraryController.cs | 7 +- API/Controllers/ReaderController.cs | 73 ++- API/Controllers/SeriesController.cs | 4 +- API/Controllers/UsersController.cs | 2 + API/DTOs/SeriesDto.cs | 6 + API/DTOs/VolumeDto.cs | 1 + API/Data/BookmarkDto.cs | 9 + API/Data/DataContext.cs | 3 +- .../20210114214506_UserProgress.Designer.cs | 576 ++++++++++++++++++ .../Migrations/20210114214506_UserProgress.cs | 84 +++ ...180406_ReadStatusModifications.Designer.cs | 562 +++++++++++++++++ .../20210117180406_ReadStatusModifications.cs | 154 +++++ .../20210117181421_SeriesPages.Designer.cs | 565 +++++++++++++++++ .../Migrations/20210117181421_SeriesPages.cs | 24 + .../Migrations/DataContextModelSnapshot.cs | 41 ++ API/Data/SeriesRepository.cs | 27 +- API/Data/UserRepository.cs | 24 +- API/Entities/AppUser.cs | 7 +- API/Entities/AppUserProgress.cs | 7 + API/Entities/Series.cs | 8 + API/Entities/Volume.cs | 3 +- API/Interfaces/ISeriesRepository.cs | 3 +- API/Services/DirectoryService.cs | 2 + 24 files changed, 2179 insertions(+), 24 deletions(-) create mode 100644 API/Data/BookmarkDto.cs create mode 100644 API/Data/Migrations/20210114214506_UserProgress.Designer.cs create mode 100644 API/Data/Migrations/20210114214506_UserProgress.cs create mode 100644 API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs create mode 100644 API/Data/Migrations/20210117180406_ReadStatusModifications.cs create mode 100644 API/Data/Migrations/20210117181421_SeriesPages.Designer.cs create mode 100644 API/Data/Migrations/20210117181421_SeriesPages.cs diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 54d304dd6..025017476 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -38,7 +38,8 @@ 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."); } @@ -88,9 +89,9 @@ namespace API.Controllers }; } - private async Task UserExists(string username) - { - return await _userManager.Users.AnyAsync(user => user.UserName == username.ToLower()); - } + // 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..1cab508b2 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -147,8 +147,13 @@ namespace API.Controllers } [HttpGet("series")] - public async Task>> GetSeriesForLibrary(int libraryId) + public async Task>> GetSeriesForLibrary(int libraryId, bool forUser = false) { + if (forUser) + { + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id)); + } return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 17a5d753d..370fe071f 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -1,7 +1,15 @@ -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.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace API.Controllers { @@ -9,11 +17,23 @@ namespace API.Controllers { private readonly IDirectoryService _directoryService; private readonly ICacheService _cacheService; + private readonly ILogger _logger; + private readonly UserManager _userManager; + private readonly DataContext _dataContext; // TODO: Refactor code into repo + private readonly IUserRepository _userRepository; + private readonly ISeriesRepository _seriesRepository; - public ReaderController(IDirectoryService directoryService, ICacheService cacheService) + public ReaderController(IDirectoryService directoryService, ICacheService cacheService, + ILogger logger, UserManager userManager, DataContext dataContext, + IUserRepository userRepository, ISeriesRepository seriesRepository) { _directoryService = directoryService; _cacheService = cacheService; + _logger = logger; + _userManager = userManager; + _dataContext = dataContext; + _userRepository = userRepository; + _seriesRepository = seriesRepository; } [HttpGet("image")] @@ -28,5 +48,54 @@ namespace API.Controllers return Ok(file); } + + [HttpGet("get-bookmark")] + public async Task> GetBookmark(int volumeId) + { + var user = await _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 _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, // TODO: PagesRead is misleading. Should it be PageNumber or PagesRead (+1)? + VolumeId = bookmarkDto.VolumeId, + SeriesId = bookmarkDto.SeriesId, + }); + } + else + { + userProgress.PagesRead = bookmarkDto.PageNum; + userProgress.SeriesId = bookmarkDto.SeriesId; + + } + + _userRepository.Update(user); + + if (await _userRepository.SaveAllAsync()) + { + 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..7df62afc2 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -19,16 +19,18 @@ namespace API.Controllers private readonly ITaskScheduler _taskScheduler; private readonly ISeriesRepository _seriesRepository; private readonly ICacheService _cacheService; + private readonly IUserRepository _userRepository; public SeriesController(ILogger logger, IMapper mapper, ITaskScheduler taskScheduler, ISeriesRepository seriesRepository, - ICacheService cacheService) + ICacheService cacheService, IUserRepository userRepository) { _logger = logger; _mapper = mapper; _taskScheduler = taskScheduler; _seriesRepository = seriesRepository; _cacheService = cacheService; + _userRepository = userRepository; } [HttpGet("{seriesId}")] diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 535345b75..c3ca25288 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -46,6 +46,8 @@ namespace API.Controllers [HttpGet("has-library-access")] public async Task> HasLibraryAccess(int libraryId) { + // TODO: refactor this to use either userexists or usermanager + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return BadRequest("Could not validate user"); diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index eb10f3d0d..942acc2f1 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -8,5 +8,11 @@ public string SortName { get; set; } public string Summary { get; set; } public byte[] CoverImage { get; set; } + + // Read Progress + public int Pages { get; set; } + public int PagesRead { get; set; } + //public int VolumesComplete { get; set; } + //public int TotalVolumes { 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/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..ff48c31af 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -46,12 +46,26 @@ namespace API.Data return _context.Series.SingleOrDefault(x => x.Name == name); } - public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId) + public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0) { - return await _context.Series - .Where(series => series.LibraryId == libraryId) + var series = await _context.Series + .Where(s => s.LibraryId == libraryId) .OrderBy(s => s.SortName) - .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + .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); + } + } + + return series; } public async Task> GetVolumesDtoAsync(int seriesId) @@ -112,5 +126,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/UserRepository.cs b/API/Data/UserRepository.cs index 5098b156c..3f04505aa 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -32,7 +32,8 @@ namespace API.Data public void Delete(AppUser user) { - _context.Users.Remove(user); + // TODO: Check how to implement for _userMangaer + _context.AppUser.Remove(user); } public async Task SaveAllAsync() @@ -42,17 +43,25 @@ namespace API.Data public async Task> GetUsersAsync() { - return await _context.Users.ToListAsync(); + return await _userManager.Users.ToListAsync(); + //return await _context.Users.ToListAsync(); } public async Task GetUserByIdAsync(int id) { - return await _context.Users.FindAsync(id); + // TODO: How to use userManager + return await _context.AppUser.FindAsync(id); } + /// + /// Gets an AppUser by username. Returns back Progress information. + /// + /// + /// public async Task GetUserByUsernameAsync(string username) { - return await _context.Users + return await _userManager.Users + .Include(u => u.Progresses) .SingleOrDefaultAsync(x => x.UserName == username); } @@ -88,11 +97,16 @@ namespace API.Data public async Task GetMemberAsync(string username) { - return await _context.Users.Where(x => x.UserName == username) + return await _userManager.Users.Where(x => x.UserName == username) .Include(x => x.Libraries) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } + + public void UpdateReadingProgressAsync(int volumeId, int pageNum) + { + + } } } \ No newline at end of file diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 9e66a7d00..46480fdd1 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -12,12 +12,15 @@ namespace API.Entities public DateTime Created { get; set; } = DateTime.Now; public DateTime LastActive { get; set; } public ICollection Libraries { get; set; } - [ConcurrencyCheck] - public uint RowVersion { get; set; } + public ICollection UserRoles { get; set; } //public ICollection SeriesProgresses { get; set; } + public ICollection Progresses { get; set; } + + [ConcurrencyCheck] + public uint RowVersion { get; set; } public void OnSavingChanges() { diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index cfff3d9d4..2f9274e26 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -5,6 +5,13 @@ /// public class AppUserProgress { + public int Id { get; set; } + public int PagesRead { get; set; } + public AppUser AppUser { get; set; } + public int AppUserId { get; set; } + public int VolumeId { get; set; } + public int SeriesId { get; set; } // shortcut + //public bool VolumeCompleted { get; set; } // This will be set true if PagesRead == Sum of MangaFiles on volume } } \ No newline at end of file diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 368a04e50..3fc8c0b72 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -26,6 +26,14 @@ 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; } + /// + /// Total Volumes linked to Entity + /// + //public int TotalVolumes { get; set; } 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..8239792f9 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -15,8 +15,7 @@ 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 public Series Series { get; set; } diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 490628163..1400431d7 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -12,7 +12,7 @@ namespace API.Interfaces Task GetSeriesByNameAsync(string name); Series GetSeriesByName(string name); bool SaveAll(); - Task> GetSeriesDtoForLibraryIdAsync(int libraryId); + Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0); Task> GetVolumesDtoAsync(int seriesId); IEnumerable GetVolumes(int seriesId); Task GetSeriesDtoByIdAsync(int seriesId); @@ -22,5 +22,6 @@ 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/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 02358862c..5828b507a 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -125,6 +125,8 @@ namespace API.Services 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); + //series.TotalVolumes = volumes.Count; return series; } From 4a2296a18a6dcce60fa026075797a9b72fa5ffe7 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 18 Jan 2021 10:46:42 -0600 Subject: [PATCH 02/11] Minor cleanup. Next commit will cleanup repositories and code base to be more concise. --- API/Controllers/SeriesController.cs | 9 +++++++-- API/Data/SeriesRepository.cs | 18 ++++++++++++++++-- API/Entities/AppUserProgress.cs | 3 +-- API/Interfaces/ISeriesRepository.cs | 4 ++-- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 7df62afc2..c8eee5db5 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -56,9 +56,14 @@ namespace API.Controllers } [HttpGet("volumes")] - public async Task>> GetVolumes(int seriesId) + public async Task>> GetVolumes(int seriesId, bool forUser = true) { - return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId)); + if (forUser) + { + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId, user.Id)); + } + return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId)); // TODO: Refactor out forUser = false since everything is user based } [HttpGet("volume")] diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index ff48c31af..2be26063d 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -68,12 +68,26 @@ namespace API.Data return series; } - public async Task> GetVolumesDtoAsync(int seriesId) + public async Task> GetVolumesDtoAsync(int seriesId, int userId = 0) { - return await _context.Volume + var volumes = await _context.Volume .Where(vol => vol.SeriesId == seriesId) .OrderBy(volume => volume.Number) .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + if (userId > 0) + { + var userProgress = await _context.AppUserProgresses + .Where(p => p.AppUserId == userId && volumes.Select(s => s.Id).Contains(p.VolumeId)) + .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) diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index 2f9274e26..e9d6a2279 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -11,7 +11,6 @@ public AppUser AppUser { get; set; } public int AppUserId { get; set; } public int VolumeId { get; set; } - public int SeriesId { get; set; } // shortcut - //public bool VolumeCompleted { get; set; } // This will be set true if PagesRead == Sum of MangaFiles on volume + public int SeriesId { get; set; } } } \ No newline at end of file diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 1400431d7..3657a2a6e 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -13,12 +13,12 @@ namespace API.Interfaces Series GetSeriesByName(string name); bool SaveAll(); Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0); - Task> GetVolumesDtoAsync(int seriesId); + Task> GetVolumesDtoAsync(int seriesId, int userId = 0); IEnumerable GetVolumes(int seriesId); Task GetSeriesDtoByIdAsync(int seriesId); Task GetVolumeAsync(int volumeId); - Task GetVolumeDtoAsync(int volumeId); + Task GetVolumeDtoAsync(int volumeId); // TODO: Likely need to update here Task> GetVolumesForSeriesAsync(int[] seriesIds); Task DeleteSeriesAsync(int seriesId); From 825afd83a2f4f21720aa96454f82e3fdaf9bf0e0 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 18 Jan 2021 13:07:48 -0600 Subject: [PATCH 03/11] Removed some dead code on the interfaces. Introduced UnitOfWork to simplify repo injection. --- API/Controllers/AccountController.cs | 16 ++-- API/Controllers/LibraryController.cs | 55 ++++++------ API/Controllers/ReaderController.cs | 23 ++--- API/Controllers/SeriesController.cs | 27 +++--- API/Controllers/UsersController.cs | 20 ++--- API/DTOs/SeriesDto.cs | 21 +++-- API/Data/UnitOfWork.cs | 36 ++++++++ API/Entities/AppUser.cs | 4 - API/Entities/AppUserProgress.cs | 7 +- API/Entities/LibraryType.cs | 4 +- API/Entities/MangaFile.cs | 5 +- API/Entities/Series.cs | 6 +- API/Entities/Volume.cs | 2 +- API/Errors/ApiException.cs | 6 +- .../ApplicationServiceExtensions.cs | 11 ++- API/Interfaces/IDirectoryService.cs | 9 +- API/Interfaces/ILibraryRepository.cs | 1 - API/Interfaces/ISeriesRepository.cs | 4 +- API/Interfaces/IUnitOfWork.cs | 13 +++ API/Interfaces/IUserRepository.cs | 4 - API/Services/CacheService.cs | 11 ++- API/Services/DirectoryService.cs | 83 +++++-------------- API/Services/TaskScheduler.cs | 1 + 23 files changed, 165 insertions(+), 204 deletions(-) create mode 100644 API/Data/UnitOfWork.cs create mode 100644 API/Interfaces/IUnitOfWork.cs diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 025017476..6ce1a4636 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -17,20 +17,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 +38,6 @@ namespace API.Controllers [HttpPost("register")] public async Task> Register(RegisterDto registerDto) { - if (await _userManager.Users.AnyAsync(x => x.UserName == registerDto.Username)) { return BadRequest("Username is taken."); @@ -77,8 +76,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}"); @@ -88,10 +87,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 1cab508b2..56ac7e49f 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -18,25 +18,21 @@ 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 IUnitOfWork _unitOfWork; private readonly ICacheService _cacheService; 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, ICacheService cacheService) { _directoryService = directoryService; - _libraryRepository = libraryRepository; _logger = logger; - _userRepository = userRepository; _mapper = mapper; _taskScheduler = taskScheduler; - _seriesRepository = seriesRepository; + _unitOfWork = unitOfWork; _cacheService = cacheService; } @@ -49,12 +45,12 @@ 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 admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList(); var library = new Library { @@ -72,15 +68,16 @@ namespace API.Controllers } - if (await _userRepository.SaveAllAsync()) + if (!await _unitOfWork.Complete()) { - _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."); } - return BadRequest("There was a critical issue. Please try again."); + _logger.LogInformation($"Created a new library: {library.Name}"); + var createdLibrary = await _unitOfWork.LibraryRepository.GetLibraryForNameAsync(library.Name); + BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id, false)); + return Ok(); + } /// @@ -105,14 +102,14 @@ namespace API.Controllers [HttpGet] public async Task>> GetLibraries() { - return Ok(await _libraryRepository.GetLibrariesAsync()); + return Ok(await _unitOfWork.LibraryRepository.GetLibrariesAsync()); } [Authorize(Policy = "RequireAdminRole")] [HttpPut("update-for")] public async Task> AddLibraryToUser(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"); @@ -123,7 +120,7 @@ namespace API.Controllers user.Libraries.Add(_mapper.Map(selectedLibrary)); } - if (await _userRepository.SaveAllAsync()) + if (await _unitOfWork.Complete()) { _logger.LogInformation($"Added: {updateLibraryForUserDto.SelectedLibraries} to {updateLibraryForUserDto.Username}"); return Ok(user); @@ -143,7 +140,7 @@ namespace API.Controllers [HttpGet("libraries-for")] public async Task>> GetLibrariesForUser(string username) { - return Ok(await _libraryRepository.GetLibrariesDtoForUsernameAsync(username)); + return Ok(await _unitOfWork.LibraryRepository.GetLibrariesDtoForUsernameAsync(username)); } [HttpGet("series")] @@ -151,10 +148,10 @@ namespace API.Controllers { if (forUser) { - var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); - return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id)); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id)); } - return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); } [Authorize(Policy = "RequireAdminRole")] @@ -163,10 +160,10 @@ 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.GetSeriesDtoForLibraryIdAsync(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()) { @@ -180,7 +177,7 @@ 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); @@ -190,9 +187,9 @@ namespace API.Controllers - _libraryRepository.Update(library); + _unitOfWork.LibraryRepository.Update(library); - if (await _libraryRepository.SaveAllAsync()) + if (await _unitOfWork.Complete()) { if (differenceBetweenFolders.Any()) { diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 370fe071f..59bb517f3 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -6,9 +6,7 @@ using API.DTOs; using API.Entities; using API.Extensions; using API.Interfaces; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace API.Controllers @@ -18,22 +16,15 @@ namespace API.Controllers private readonly IDirectoryService _directoryService; private readonly ICacheService _cacheService; private readonly ILogger _logger; - private readonly UserManager _userManager; - private readonly DataContext _dataContext; // TODO: Refactor code into repo - private readonly IUserRepository _userRepository; - private readonly ISeriesRepository _seriesRepository; + private readonly IUnitOfWork _unitOfWork; public ReaderController(IDirectoryService directoryService, ICacheService cacheService, - ILogger logger, UserManager userManager, DataContext dataContext, - IUserRepository userRepository, ISeriesRepository seriesRepository) + ILogger logger, IUnitOfWork unitOfWork) { _directoryService = directoryService; _cacheService = cacheService; _logger = logger; - _userManager = userManager; - _dataContext = dataContext; - _userRepository = userRepository; - _seriesRepository = seriesRepository; + _unitOfWork = unitOfWork; } [HttpGet("image")] @@ -52,7 +43,7 @@ namespace API.Controllers [HttpGet("get-bookmark")] public async Task> GetBookmark(int volumeId) { - var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + 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); @@ -64,7 +55,7 @@ namespace API.Controllers [HttpPost("bookmark")] public async Task Bookmark(BookmarkDto bookmarkDto) { - var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + 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(); @@ -87,9 +78,9 @@ namespace API.Controllers } - _userRepository.Update(user); + _unitOfWork.UserRepository.Update(user); - if (await _userRepository.SaveAllAsync()) + if (await _unitOfWork.Complete()) { return Ok(); } diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index c8eee5db5..f93f4700b 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -15,28 +15,23 @@ 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 IUserRepository _userRepository; + private readonly IUnitOfWork _unitOfWork; - public SeriesController(ILogger logger, IMapper mapper, - ITaskScheduler taskScheduler, ISeriesRepository seriesRepository, - ICacheService cacheService, IUserRepository userRepository) + public SeriesController(ILogger logger, ITaskScheduler taskScheduler, + ICacheService cacheService, IUnitOfWork unitOfWork) { _logger = logger; - _mapper = mapper; _taskScheduler = taskScheduler; - _seriesRepository = seriesRepository; _cacheService = cacheService; - _userRepository = userRepository; + _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")] @@ -44,9 +39,9 @@ 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) { @@ -60,16 +55,16 @@ namespace API.Controllers { if (forUser) { - var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); - return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId, user.Id)); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id)); } - return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId)); // TODO: Refactor out forUser = false since everything is user based + return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId)); // TODO: Refactor out forUser = false since everything is user based } [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 c3ca25288..07c3570f8 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -12,23 +12,21 @@ 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 _userRepository.SaveAllAsync()) + if (await _unitOfWork.Complete()) { return Ok(); } @@ -40,7 +38,7 @@ namespace API.Controllers [HttpGet] public async Task>> GetUsers() { - return Ok(await _userRepository.GetMembersAsync()); + return Ok(await _unitOfWork.UserRepository.GetMembersAsync()); } [HttpGet("has-library-access")] @@ -48,11 +46,11 @@ namespace API.Controllers { // TODO: refactor this to use either userexists or usermanager - var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + var user = await _unitOfWork.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.GetLibrariesDtoForUsernameAsync(user.UserName); return Ok(libs.Any(x => x.Id == libraryId)); } diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 942acc2f1..50160ab5c 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -2,17 +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; } - - // Read Progress - public int Pages { 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; } - //public int VolumesComplete { get; set; } - //public int TotalVolumes { get; set; } } } \ No newline at end of file diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs new file mode 100644 index 000000000..77f461f7b --- /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, _mapper, _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/Entities/AppUser.cs b/API/Entities/AppUser.cs index 46480fdd1..10bcec503 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -12,11 +12,7 @@ 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 SeriesProgresses { get; set; } public ICollection Progresses { get; set; } [ConcurrencyCheck] diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index e9d6a2279..5c8d2c05c 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -7,10 +7,11 @@ { public int Id { get; set; } public int PagesRead { get; set; } - - public AppUser AppUser { get; set; } - public int AppUserId { 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 3fc8c0b72..8fe8e6628 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -30,10 +30,8 @@ namespace API.Entities /// Sum of all Volume pages /// public int Pages { get; set; } - /// - /// Total Volumes linked to Entity - /// - //public int TotalVolumes { 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 8239792f9..304c2bfae 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -17,7 +17,7 @@ namespace API.Entities - // 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..cb007b1a5 100644 --- a/API/Interfaces/IDirectoryService.cs +++ b/API/Interfaces/IDirectoryService.cs @@ -13,13 +13,8 @@ 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); + + //IList ListFiles(string rootPath); /// /// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 5a919770c..5050c255b 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -8,7 +8,6 @@ namespace API.Interfaces public interface ILibraryRepository { void Update(Library library); - Task SaveAllAsync(); Task> GetLibrariesAsync(); Task LibraryExists(string libraryName); Task GetLibraryForIdAsync(int libraryId); diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 3657a2a6e..d2fe75353 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -8,17 +8,15 @@ namespace API.Interfaces public interface ISeriesRepository { void Update(Series series); - Task SaveAllAsync(); Task GetSeriesByNameAsync(string name); Series GetSeriesByName(string name); - bool SaveAll(); Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0); Task> GetVolumesDtoAsync(int seriesId, int userId = 0); IEnumerable GetVolumes(int seriesId); Task GetSeriesDtoByIdAsync(int seriesId); Task GetVolumeAsync(int volumeId); - Task GetVolumeDtoAsync(int volumeId); // TODO: Likely need to update here + Task GetVolumeDtoAsync(int volumeId); Task> GetVolumesForSeriesAsync(int[] seriesIds); Task DeleteSeriesAsync(int seriesId); 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..43232e9cc 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -8,12 +8,8 @@ namespace API.Interfaces public interface IUserRepository { void Update(AppUser user); - Task SaveAllAsync(); - Task> GetUsersAsync(); - Task GetUserByIdAsync(int id); Task GetUserByUsernameAsync(string username); Task> GetMembersAsync(); - Task GetMemberAsync(string username); public void Delete(AppUser user); Task> GetAdminUsersAsync(); } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index a6372bb1f..3572d7567 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,8 +109,7 @@ namespace API.Services if (page + 1 < (mangaFile.NumberOfPages + pagesSoFar)) { var path = GetVolumeCachePath(volume.Id, mangaFile); - - var files = _directoryService.ListFiles(path); + var files = DirectoryService.GetFiles(path); var array = files.ToArray(); Array.Sort(array, _numericComparer); // TODO: Find a way to apply numericComparer to IList. diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 5828b507a..d76dfd65b 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -23,31 +23,28 @@ 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, - string searchPatternExpression = "", + public static IEnumerable GetFiles(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 => @@ -67,11 +64,11 @@ namespace API.Services return dirs; } - public IList ListFiles(string rootPath) - { - if (!Directory.Exists(rootPath)) return ImmutableList.Empty; - return Directory.GetFiles(rootPath); - } + // public IList ListFiles(string rootPath) + // { + // if (!Directory.Exists(rootPath)) return ImmutableList.Empty; + // return Directory.GetFiles(rootPath); + // } /// @@ -114,7 +111,7 @@ namespace API.Services private Series UpdateSeries(string seriesName, ParserInfo[] infos, bool forceUpdate) { - var series = _seriesRepository.GetSeriesByName(seriesName) ?? new Series + var series = _unitOfWork.SeriesRepository.GetSeriesByName(seriesName) ?? new Series { Name = seriesName, OriginalName = seriesName, @@ -126,7 +123,7 @@ namespace API.Services series.Volumes = volumes; series.CoverImage = volumes.OrderBy(x => x.Number).FirstOrDefault()?.CoverImage; series.Pages = volumes.Sum(v => v.Pages); - //series.TotalVolumes = volumes.Count; + return series; } @@ -154,7 +151,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) { @@ -220,7 +217,7 @@ namespace API.Services Library library; try { - library = Task.Run(() => _libraryRepository.GetLibraryForIdAsync(libraryId)).Result; + library = Task.Run(() => _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId)).Result; } catch (Exception ex) { @@ -264,12 +261,10 @@ namespace API.Services _logger.LogInformation($"Created/Updated series {mangaSeries.Name}"); library.Series.Add(mangaSeries); } + + _unitOfWork.LibraryRepository.Update(library); - - - _libraryRepository.Update(library); - - if (_libraryRepository.SaveAll()) + if (Task.Run(() => _unitOfWork.Complete()).Result) { _logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series."); } @@ -287,46 +282,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)) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 00fd5597e..cb89df4d8 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -16,6 +16,7 @@ namespace API.Services _logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis."); RecurringJob.AddOrUpdate(() => cacheService.Cleanup(), Cron.Daily); + //RecurringJob.AddOrUpdate(() => scanService.ScanLibraries(), Cron.Daily); } From 26660a9bb300f8a7b10602afa3b6416da851b095 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 18 Jan 2021 13:53:24 -0600 Subject: [PATCH 04/11] Further cleanup. Moved BackgroundJob Task enqueues into TaskScheduler, so I can have complete control via one interface. --- API/Controllers/LibraryController.cs | 36 +++++++++++++--------------- API/Controllers/SeriesController.cs | 2 +- API/Controllers/UsersController.cs | 2 +- API/Data/LibraryRepository.cs | 15 +++--------- API/Interfaces/ILibraryRepository.cs | 6 ++--- API/Interfaces/ITaskScheduler.cs | 4 +++- API/Services/TaskScheduler.cs | 25 +++++++++++++++---- 7 files changed, 47 insertions(+), 43 deletions(-) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 56ac7e49f..f72a7df63 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -74,10 +74,8 @@ namespace API.Controllers } _logger.LogInformation($"Created a new library: {library.Name}"); - var createdLibrary = await _unitOfWork.LibraryRepository.GetLibraryForNameAsync(library.Name); - BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id, false)); + _taskScheduler.ScanLibrary(library.Id); return Ok(); - } /// @@ -89,6 +87,7 @@ namespace API.Controllers [HttpGet("list")] public ActionResult> GetDirectories(string path) { + // TODO: Move this to another controller. if (string.IsNullOrEmpty(path)) { return Ok(Directory.GetLogicalDrives()); @@ -102,11 +101,11 @@ namespace API.Controllers [HttpGet] public async Task>> GetLibraries() { - return Ok(await _unitOfWork.LibraryRepository.GetLibrariesAsync()); + return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()); } [Authorize(Policy = "RequireAdminRole")] - [HttpPut("update-for")] + [HttpPut("grant-access")] public async Task> AddLibraryToUser(UpdateLibraryForUserDto updateLibraryForUserDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username); @@ -133,19 +132,21 @@ namespace API.Controllers [HttpPost("scan")] public ActionResult Scan(int libraryId) { - BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(libraryId, true)); + //BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(libraryId, true)); + _taskScheduler.ScanLibrary(libraryId, true); return Ok(); } [HttpGet("libraries-for")] public async Task>> GetLibrariesForUser(string username) { - return Ok(await _unitOfWork.LibraryRepository.GetLibrariesDtoForUsernameAsync(username)); + return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username)); } [HttpGet("series")] public async Task>> GetSeriesForLibrary(int libraryId, bool forUser = false) { + // TODO: Move to series? if (forUser) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); @@ -167,7 +168,7 @@ namespace API.Controllers if (result && volumes.Any()) { - BackgroundJob.Enqueue(() => _cacheService.CleanupVolumes(volumes)); + _taskScheduler.CleanupVolumes(volumes); } return Ok(result); @@ -184,22 +185,17 @@ namespace API.Controllers library.Name = libraryForUserDto.Name; library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList(); - - - + _unitOfWork.LibraryRepository.Update(library); - if (await _unitOfWork.Complete()) + 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/SeriesController.cs b/API/Controllers/SeriesController.cs index f93f4700b..37064489a 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -45,7 +45,7 @@ namespace API.Controllers if (result) { - BackgroundJob.Enqueue(() => _cacheService.CleanupVolumes(volumes)); + _taskScheduler.CleanupVolumes(volumes); } return Ok(result); } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 07c3570f8..e199a1a44 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -50,7 +50,7 @@ namespace API.Controllers if (user == null) return BadRequest("Could not validate user"); - var libs = await _unitOfWork.LibraryRepository.GetLibrariesDtoForUsernameAsync(user.UserName); + var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(user.UserName); return Ok(libs.Any(x => x.Id == libraryId)); } diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index 15cd38d8b..32c65ffbd 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -26,23 +26,14 @@ namespace API.Data _context.Entry(library).State = EntityState.Modified; } - public async Task SaveAllAsync() - { - return await _context.SaveChangesAsync() > 0; - } - - public bool SaveAll() - { - return _context.SaveChanges() > 0; - } - - public async Task> GetLibrariesDtoForUsernameAsync(string userName) + public async Task> GetLibraryDtosForUsernameAsync(string userName) { // TODO: Speed this query up return await _context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(x => x.UserName == userName)) .ProjectTo(_mapper.ConfigurationProvider) + .AsNoTracking() .ToListAsync(); } @@ -62,7 +53,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) diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 5050c255b..4b0a962f6 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -8,13 +8,11 @@ namespace API.Interfaces public interface ILibraryRepository { void Update(Library library); - Task> GetLibrariesAsync(); + Task> GetLibraryDtosAsync(); Task LibraryExists(string libraryName); Task GetLibraryForIdAsync(int libraryId); - bool SaveAll(); - Task> GetLibrariesDtoForUsernameAsync(string userName); + Task> GetLibraryDtosForUsernameAsync(string userName); Task GetLibraryForNameAsync(string libraryName); - Task DeleteLibrary(int libraryId); } } \ 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/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index cb89df4d8..aa5957f58 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -6,19 +6,36 @@ namespace API.Services { public class TaskScheduler : ITaskScheduler { + private readonly ICacheService _cacheService; private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; + private readonly IDirectoryService _directoryService; private readonly BackgroundJobServer _client; - public TaskScheduler(ICacheService cacheService, ILogger logger) + public TaskScheduler(ICacheService cacheService, ILogger logger, + IUnitOfWork unitOfWork, IDirectoryService directoryService) { + _cacheService = cacheService; _logger = logger; + _unitOfWork = unitOfWork; + _directoryService = directoryService; _client = new BackgroundJobServer(); _logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis."); - RecurringJob.AddOrUpdate(() => cacheService.Cleanup(), Cron.Daily); + RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily); //RecurringJob.AddOrUpdate(() => scanService.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 From 80283bcd497f5bc45984de38173dda6983eac8ef Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 18 Jan 2021 16:55:52 -0600 Subject: [PATCH 05/11] When registering an admin user, ensure they have access to all libraries. --- API/Controllers/AccountController.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 6ce1a4636..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; @@ -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, From 295e62d773f3d175aae5b96a7bae292c3c39ef53 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 18 Jan 2021 17:18:42 -0600 Subject: [PATCH 06/11] Fixed grant-access api and new library to properly update the db. Somehow the old way of updating db no longer works. --- API/Controllers/LibraryController.cs | 60 +++++++++++++++++----------- API/Data/LibraryRepository.cs | 7 ++++ API/Data/UserRepository.cs | 43 +++++--------------- API/Helpers/AutoMapperProfiles.cs | 7 ++++ API/Interfaces/ILibraryRepository.cs | 1 + API/Interfaces/IUserRepository.cs | 2 +- API/Services/TaskScheduler.cs | 5 +-- 7 files changed, 64 insertions(+), 61 deletions(-) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index f72a7df63..30b946739 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; @@ -60,14 +61,7 @@ namespace API.Controllers Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList() }; - foreach (var admin in admins) - { - // If user is null, then set it - admin.Libraries ??= new List(); - admin.Libraries.Add(library); - } - - + _unitOfWork.LibraryRepository.Update(library); if (!await _unitOfWork.Complete()) { return BadRequest("There was a critical issue. Please try again."); @@ -87,7 +81,6 @@ namespace API.Controllers [HttpGet("list")] public ActionResult> GetDirectories(string path) { - // TODO: Move this to another controller. if (string.IsNullOrEmpty(path)) { return Ok(Directory.GetLogicalDrives()); @@ -105,26 +98,46 @@ namespace API.Controllers } [Authorize(Policy = "RequireAdminRole")] - [HttpPut("grant-access")] - public async Task> AddLibraryToUser(UpdateLibraryForUserDto updateLibraryForUserDto) + [HttpPost("grant-access")] + public async Task> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto) { 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 (!_unitOfWork.HasChanges()) + { + _logger.LogInformation($"Added: {updateLibraryForUserDto.SelectedLibraries} to {updateLibraryForUserDto.Username}"); + return Ok(_mapper.Map(user)); + } + if (await _unitOfWork.Complete()) { _logger.LogInformation($"Added: {updateLibraryForUserDto.SelectedLibraries} to {updateLibraryForUserDto.Username}"); - return Ok(user); + return Ok(_mapper.Map(user)); } - + + return BadRequest("There was a critical issue. Please try again."); } @@ -132,15 +145,14 @@ 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 _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username)); + return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername())); } [HttpGet("series")] diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index 32c65ffbd..5847430c4 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -36,6 +36,13 @@ namespace API.Data .AsNoTracking() .ToListAsync(); } + + public async Task> GetLibrariesAsync() + { + return await _context.Library + .Include(l => l.AppUsers) + .ToListAsync(); + } public async Task GetLibraryForNameAsync(string libraryName) { diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 3f04505aa..72bcdcc63 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -32,27 +32,9 @@ namespace API.Data public void Delete(AppUser user) { - // TODO: Check how to implement for _userMangaer _context.AppUser.Remove(user); } - public async Task SaveAllAsync() - { - return await _context.SaveChangesAsync() > 0; - } - - public async Task> GetUsersAsync() - { - return await _userManager.Users.ToListAsync(); - //return await _context.Users.ToListAsync(); - } - - public async Task GetUserByIdAsync(int id) - { - // TODO: How to use userManager - return await _context.AppUser.FindAsync(id); - } - /// /// Gets an AppUser by username. Returns back Progress information. /// @@ -60,7 +42,7 @@ namespace API.Data /// public async Task GetUserByUsernameAsync(string username) { - return await _userManager.Users + return await _context.Users .Include(u => u.Progresses) .SingleOrDefaultAsync(x => x.UserName == username); } @@ -72,7 +54,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) @@ -92,21 +74,16 @@ namespace API.Data Folders = l.Folders.Select(x => x.Path).ToList() }).ToList() }) + .AsNoTracking() .ToListAsync(); } - public async Task GetMemberAsync(string username) - { - return await _userManager.Users.Where(x => x.UserName == username) - .Include(x => x.Libraries) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); - } - - public void UpdateReadingProgressAsync(int volumeId, int pageNum) - { - - } - + // 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/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 714871813..586dc04f0 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -10,6 +10,13 @@ namespace API.Helpers public AutoMapperProfiles() { CreateMap(); + // .ForMember(dest => dest.Folders, + // opt => + // opt.MapFrom(src => src.Folders.Select(x => new FolderPath() + // { + // Path = x, + // //LibraryId = src.Id + // }).ToList())); CreateMap(); diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 4b0a962f6..6ebc478e3 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -12,6 +12,7 @@ namespace API.Interfaces Task LibraryExists(string libraryName); Task GetLibraryForIdAsync(int libraryId); Task> GetLibraryDtosForUsernameAsync(string userName); + Task> GetLibrariesAsync(); Task GetLibraryForNameAsync(string libraryName); Task DeleteLibrary(int libraryId); } diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 43232e9cc..01127050a 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -8,9 +8,9 @@ namespace API.Interfaces public interface IUserRepository { void Update(AppUser user); + public void Delete(AppUser user); Task GetUserByUsernameAsync(string username); Task> GetMembersAsync(); - public void Delete(AppUser user); Task> GetAdminUsersAsync(); } } \ No newline at end of file diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index aa5957f58..d33b82758 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -10,7 +10,7 @@ namespace API.Services private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; private readonly IDirectoryService _directoryService; - private readonly BackgroundJobServer _client; + public BackgroundJobServer Client => new BackgroundJobServer(); public TaskScheduler(ICacheService cacheService, ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService) @@ -19,8 +19,7 @@ namespace API.Services _logger = logger; _unitOfWork = unitOfWork; _directoryService = directoryService; - _client = new BackgroundJobServer(); - + _logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis."); RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily); //RecurringJob.AddOrUpdate(() => scanService.ScanLibraries(), Cron.Daily); From 14e8c3b8205da25c1816b21ade30944534b4e9e3 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 19 Jan 2021 10:45:37 -0600 Subject: [PATCH 07/11] Fixed some APIs that worked mins ago....something strange happening with EF relationships. --- API/Controllers/LibraryController.cs | 22 ++++++++------- API/Controllers/SeriesController.cs | 10 +++---- API/Controllers/UsersController.cs | 16 +++-------- API/Data/DataContext.cs | 5 ++++ API/Data/LibraryRepository.cs | 5 ++++ API/Data/SeriesRepository.cs | 40 +++++++++++++++++++--------- API/Entities/AppUserProgress.cs | 4 ++- API/Entities/Library.cs | 1 + API/Interfaces/ILibraryRepository.cs | 1 + API/Services/DirectoryService.cs | 1 + 10 files changed, 62 insertions(+), 43 deletions(-) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 30b946739..d69e98b6e 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -8,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; @@ -50,23 +49,26 @@ namespace API.Controllers { return BadRequest("Library name already exists. Please choose a unique name to the server."); } - - var admins = (await _unitOfWork.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.Update(library); - if (!await _unitOfWork.Complete()) + _unitOfWork.LibraryRepository.Add(library); + + var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList(); + foreach (var admin in admins) { - return BadRequest("There was a critical issue. Please try again."); + admin.Libraries ??= new List(); + admin.Libraries.Add(library); } + + 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(); @@ -158,13 +160,13 @@ namespace API.Controllers [HttpGet("series")] public async Task>> GetSeriesForLibrary(int libraryId, bool forUser = false) { - // TODO: Move to series? + int userId = 0; if (forUser) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id)); + userId = user.Id; } - return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId)); } [Authorize(Policy = "RequireAdminRole")] diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 37064489a..589362a12 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -51,14 +51,10 @@ namespace API.Controllers } [HttpGet("volumes")] - public async Task>> GetVolumes(int seriesId, bool forUser = true) + public async Task>> GetVolumes(int seriesId) { - if (forUser) - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id)); - } - return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId)); // TODO: Refactor out forUser = false since everything is user based + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id)); } [HttpGet("volume")] diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index e199a1a44..6a98eedd9 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -26,11 +26,8 @@ namespace API.Controllers var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username); _unitOfWork.UserRepository.Delete(user); - if (await _unitOfWork.Complete()) - { - return Ok(); - } - + if (await _unitOfWork.Complete()) return Ok(); + return BadRequest("Could not delete the user."); } @@ -44,14 +41,7 @@ namespace API.Controllers [HttpGet("has-library-access")] public async Task> HasLibraryAccess(int libraryId) { - // TODO: refactor this to use either userexists or usermanager - - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - - if (user == null) return BadRequest("Could not validate user"); - - var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(user.UserName); - + var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()); return Ok(libs.Any(x => x.Id == libraryId)); } } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 2bb9424c0..97a1c32b8 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -39,6 +39,11 @@ namespace API.Data .WithOne(u => u.Role) .HasForeignKey(ur => ur.RoleId) .IsRequired(); + // AppUsers have Libraries, not other way around + builder.Entity() + .HasMany(p => p.AppUsers) + .WithMany(p => p.Libraries) + .UsingEntity(j => j.ToTable("AppUserLibrary")); } void OnEntityTracked(object sender, EntityTrackedEventArgs e) diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index 5847430c4..54fc64fea 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -20,6 +20,11 @@ namespace API.Data _context = context; _mapper = mapper; } + + public void Add(Library library) + { + _context.Library.Add(library); + } public void Update(Library library) { diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index 2be26063d..060175c20 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; @@ -48,11 +50,24 @@ namespace API.Data public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0) { + // if (userId > 0) + // { + // return await _context.AppUserProgresses + // .Include(p => p.Series) + // .Where(p => p.AppUserId == userId && p.Series.LibraryId == libraryId) + // .Select(p => p.Series) + // .ProjectTo(_mapper.ConfigurationProvider) + // //.Select(s => s.PagesRead = ) + // .ToListAsync(); + // } + + 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 @@ -65,25 +80,26 @@ namespace API.Data } } + Console.WriteLine("Processed GetSeriesDtoForLibraryIdAsync in {0} milliseconds", sw.ElapsedMilliseconds); return series; } - public async Task> GetVolumesDtoAsync(int seriesId, int userId = 0) + public async Task> GetVolumesDtoAsync(int seriesId, int userId) { var volumes = await _context.Volume .Where(vol => vol.SeriesId == seriesId) .OrderBy(volume => volume.Number) - .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); - if (userId > 0) - { - var userProgress = await _context.AppUserProgresses - .Where(p => p.AppUserId == userId && volumes.Select(s => s.Id).Contains(p.VolumeId)) - .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); - } + foreach (var v in volumes) + { + v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead); } return volumes; diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index 5c8d2c05c..8097c5abf 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -1,4 +1,6 @@ -namespace API.Entities +using System.ComponentModel.DataAnnotations.Schema; + +namespace API.Entities { /// /// Represents the progress a single user has on a given Volume. diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 3c2129c37..9c93fa337 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using API.Entities.Interfaces; namespace API.Entities diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 6ebc478e3..1a0d3f778 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -7,6 +7,7 @@ namespace API.Interfaces { public interface ILibraryRepository { + void Add(Library library); void Update(Library library); Task> GetLibraryDtosAsync(); Task LibraryExists(string libraryName); diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index d76dfd65b..e49e36c99 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -257,6 +257,7 @@ namespace API.Services library.Series = new List(); // Temp delete everything until we can mark items Unavailable foreach (var seriesKey in series.Keys) { + // TODO: Critical bug: Code is not taking libraryId into account and series are being linked across libraries. var mangaSeries = UpdateSeries(seriesKey, series[seriesKey].ToArray(), forceUpdate); _logger.LogInformation($"Created/Updated series {mangaSeries.Name}"); library.Series.Add(mangaSeries); From c75feb03e16fe9fd74ee0343c2f57d0bbed4d3ec Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 19 Jan 2021 12:06:45 -0600 Subject: [PATCH 08/11] Fixed offset bug in GetCachedPagePath for if you've read just one page. Fixed a bad refactor for getting files. --- API/Controllers/LibraryController.cs | 11 +++-------- API/Data/DataContext.cs | 8 ++++---- API/Data/LibraryRepository.cs | 10 +++++++--- API/Services/CacheService.cs | 2 +- API/Services/DirectoryService.cs | 14 +++++++++++--- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index d69e98b6e..acf0ce24c 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -158,15 +158,10 @@ namespace API.Controllers } [HttpGet("series")] - public async Task>> GetSeriesForLibrary(int libraryId, bool forUser = false) + public async Task>> GetSeriesForLibrary(int libraryId) { - int userId = 0; - if (forUser) - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - userId = user.Id; - } - return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId)); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id)); } [Authorize(Policy = "RequireAdminRole")] diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 97a1c32b8..09a53f0cd 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -40,10 +40,10 @@ namespace API.Data .HasForeignKey(ur => ur.RoleId) .IsRequired(); // AppUsers have Libraries, not other way around - builder.Entity() - .HasMany(p => p.AppUsers) - .WithMany(p => p.Libraries) - .UsingEntity(j => j.ToTable("AppUserLibrary")); + // builder.Entity() + // .HasMany(p => p.AppUsers) + // .WithMany(p => p.Libraries) + // .UsingEntity(j => j.ToTable("AppUserLibrary")); } void OnEntityTracked(object sender, EntityTrackedEventArgs e) diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index 54fc64fea..ef21ffd55 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; @@ -33,13 +35,15 @@ namespace API.Data public async Task> GetLibraryDtosForUsernameAsync(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() diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 3572d7567..0c69abae8 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -113,7 +113,7 @@ namespace API.Services var array = files.ToArray(); Array.Sort(array, _numericComparer); // TODO: Find a way to apply numericComparer to IList. - return array.ElementAt((page + 1) - pagesSoFar); + return array.ElementAt(page - pagesSoFar); } pagesSoFar += mangaFile.NumberOfPages; diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index e49e36c99..25c6349c9 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -40,8 +40,8 @@ namespace API.Services /// Regex version of search pattern (ie \.mp3|\.mp4). Defaults to * meaning all files. /// SearchOption to use, defaults to TopDirectoryOnly /// List of file paths - public static IEnumerable GetFiles(string path, - string searchPatternExpression = "*", + public static IEnumerable GetFilesWithCertainExtensions(string path, + string searchPatternExpression = "", SearchOption searchOption = SearchOption.TopDirectoryOnly) { if (!Directory.Exists(path)) return ImmutableList.Empty; @@ -50,6 +50,14 @@ namespace API.Services .Where(file => reSearchPattern.IsMatch(Path.GetExtension(file))); } + + public static IList GetFiles(string path) + { + if (!Directory.Exists(path)) return ImmutableList.Empty; + return Directory.GetFiles(path); + } + + public IEnumerable ListDirectory(string rootPath) { @@ -384,7 +392,7 @@ namespace API.Services } try { - files = DirectoryService.GetFiles(currentDir, Parser.Parser.MangaFileExtensions) + files = DirectoryService.GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions) .ToArray(); } catch (UnauthorizedAccessException e) { From 44ebca36ecf42256dc49ae0407aed5b6c1e663da Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 19 Jan 2021 12:51:41 -0600 Subject: [PATCH 09/11] Cleaned up some TODOs. --- API/Controllers/ReaderController.cs | 2 +- API/Services/CacheService.cs | 5 ++--- API/Services/DirectoryService.cs | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 59bb517f3..d2fed05f7 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -66,7 +66,7 @@ namespace API.Controllers user.Progresses.Add(new AppUserProgress { - PagesRead = bookmarkDto.PageNum, // TODO: PagesRead is misleading. Should it be PageNumber or PagesRead (+1)? + PagesRead = bookmarkDto.PageNum, VolumeId = bookmarkDto.VolumeId, SeriesId = bookmarkDto.SeriesId, }); diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 0c69abae8..ae92e3261 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -110,10 +110,9 @@ namespace API.Services { var path = GetVolumeCachePath(volume.Id, mangaFile); var files = DirectoryService.GetFiles(path); - var array = files.ToArray(); - Array.Sort(array, _numericComparer); // TODO: Find a way to apply numericComparer to IList. + Array.Sort(files, _numericComparer); - return array.ElementAt(page - pagesSoFar); + return files.ElementAt(page - pagesSoFar); } pagesSoFar += mangaFile.NumberOfPages; diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 25c6349c9..a67cd66a2 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -51,9 +51,9 @@ namespace API.Services reSearchPattern.IsMatch(Path.GetExtension(file))); } - public static IList GetFiles(string path) + public static string[] GetFiles(string path) { - if (!Directory.Exists(path)) return ImmutableList.Empty; + if (!Directory.Exists(path)) return Array.Empty(); return Directory.GetFiles(path); } From e180032a8e62958816404d83f051c41f0a68fb58 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 19 Jan 2021 14:35:24 -0600 Subject: [PATCH 10/11] ScanLibrary now respects the library a series belongs to, doesn't reset series every run but updates/removes/inserts as needed. --- API/Controllers/LibraryController.cs | 2 +- API/Data/LibraryRepository.cs | 1 + API/Data/SeriesRepository.cs | 15 ++++++- API/Interfaces/ISeriesRepository.cs | 5 ++- API/Services/DirectoryService.cs | 60 ++++++++++++++++------------ 5 files changed, 54 insertions(+), 29 deletions(-) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index acf0ce24c..f6e8afd9a 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -170,7 +170,7 @@ namespace API.Controllers { var username = User.GetUsername(); _logger.LogInformation($"Library {libraryId} is being deleted by {username}."); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId); + 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 _unitOfWork.LibraryRepository.DeleteLibrary(libraryId); diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index ef21ffd55..a85435df2 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -81,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/SeriesRepository.cs b/API/Data/SeriesRepository.cs index 060175c20..d5106e15e 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -23,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; @@ -48,7 +53,15 @@ namespace API.Data return _context.Series.SingleOrDefault(x => x.Name == name); } - public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0) + public async Task> GetSeriesForLibraryIdAsync(int libraryId) + { + return await _context.Series + .Where(s => s.LibraryId == libraryId) + .OrderBy(s => s.SortName) + .ToListAsync(); + } + + public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId) { // if (userId > 0) // { diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index d2fe75353..1e72cef3c 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -7,10 +7,12 @@ namespace API.Interfaces { public interface ISeriesRepository { + void Add(Series series); void Update(Series series); Task GetSeriesByNameAsync(string name); Series GetSeriesByName(string name); - Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0); + Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId); + Task> GetSeriesForLibraryIdAsync(int libraryId); Task> GetVolumesDtoAsync(int seriesId, int userId = 0); IEnumerable GetVolumes(int seriesId); Task GetSeriesDtoByIdAsync(int seriesId); @@ -21,5 +23,6 @@ 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/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index a67cd66a2..d652ee149 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -40,7 +40,7 @@ namespace API.Services /// Regex version of search pattern (ie \.mp3|\.mp4). Defaults to * meaning all files. /// SearchOption to use, defaults to TopDirectoryOnly /// List of file paths - public static IEnumerable GetFilesWithCertainExtensions(string path, + private static IEnumerable GetFilesWithCertainExtensions(string path, string searchPatternExpression = "", SearchOption searchOption = SearchOption.TopDirectoryOnly) { @@ -57,9 +57,7 @@ namespace API.Services return Directory.GetFiles(path); } - - - public IEnumerable ListDirectory(string rootPath) + public IEnumerable ListDirectory(string rootPath) { if (!Directory.Exists(rootPath)) return ImmutableList.Empty; @@ -72,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. /// @@ -117,20 +108,20 @@ namespace API.Services } } - private Series UpdateSeries(string seriesName, ParserInfo[] infos, bool forceUpdate) + private Series UpdateSeries(Series series, ParserInfo[] infos, bool forceUpdate) { - var series = _unitOfWork.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; } @@ -262,17 +253,34 @@ 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) { - // TODO: Critical bug: Code is not taking libraryId into account and series are being linked across libraries. - 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); } + + // 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."); From 3c8e4b2240f50bb323e705dd7917b6387ccd533f Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 19 Jan 2021 14:41:50 -0600 Subject: [PATCH 11/11] Cleaned up some warnings and implemented re-occuring scan libraries task. Customization of task schedules is in v0.2. --- API/Controllers/LibraryController.cs | 4 +--- API/Controllers/SeriesController.cs | 7 +------ API/Data/DataContext.cs | 5 ----- API/Data/SeriesRepository.cs | 11 ----------- API/Data/UnitOfWork.cs | 2 +- API/Data/UserRepository.cs | 14 +------------- API/Entities/AppUserProgress.cs | 3 +-- API/Entities/Library.cs | 1 - API/Helpers/AutoMapperProfiles.cs | 7 ------- API/Interfaces/IDirectoryService.cs | 5 ++--- API/Interfaces/ISeriesRepository.cs | 2 +- API/Services/DirectoryService.cs | 11 ++++++++++- API/Services/TaskScheduler.cs | 6 ++---- 13 files changed, 20 insertions(+), 58 deletions(-) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index f6e8afd9a..67990db2e 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -22,18 +22,16 @@ namespace API.Controllers private readonly IMapper _mapper; private readonly ITaskScheduler _taskScheduler; private readonly IUnitOfWork _unitOfWork; - private readonly ICacheService _cacheService; public LibraryController(IDirectoryService directoryService, ILogger logger, IMapper mapper, ITaskScheduler taskScheduler, - IUnitOfWork unitOfWork, ICacheService cacheService) + IUnitOfWork unitOfWork) { _directoryService = directoryService; _logger = logger; _mapper = mapper; _taskScheduler = taskScheduler; _unitOfWork = unitOfWork; - _cacheService = cacheService; } /// diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 589362a12..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; @@ -16,15 +14,12 @@ namespace API.Controllers { private readonly ILogger _logger; private readonly ITaskScheduler _taskScheduler; - private readonly ICacheService _cacheService; private readonly IUnitOfWork _unitOfWork; - public SeriesController(ILogger logger, ITaskScheduler taskScheduler, - ICacheService cacheService, IUnitOfWork unitOfWork) + public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork) { _logger = logger; _taskScheduler = taskScheduler; - _cacheService = cacheService; _unitOfWork = unitOfWork; } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 09a53f0cd..2bb9424c0 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -39,11 +39,6 @@ namespace API.Data .WithOne(u => u.Role) .HasForeignKey(ur => ur.RoleId) .IsRequired(); - // AppUsers have Libraries, not other way around - // builder.Entity() - // .HasMany(p => p.AppUsers) - // .WithMany(p => p.Libraries) - // .UsingEntity(j => j.ToTable("AppUserLibrary")); } void OnEntityTracked(object sender, EntityTrackedEventArgs e) diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index d5106e15e..510a21388 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -63,17 +63,6 @@ namespace API.Data public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId) { - // if (userId > 0) - // { - // return await _context.AppUserProgresses - // .Include(p => p.Series) - // .Where(p => p.AppUserId == userId && p.Series.LibraryId == libraryId) - // .Select(p => p.Series) - // .ProjectTo(_mapper.ConfigurationProvider) - // //.Select(s => s.PagesRead = ) - // .ToListAsync(); - // } - var sw = Stopwatch.StartNew(); var series = await _context.Series .Where(s => s.LibraryId == libraryId) diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 77f461f7b..25d0002c7 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -20,7 +20,7 @@ namespace API.Data } public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper); - public IUserRepository UserRepository => new UserRepository(_context, _mapper, _userManager); + public IUserRepository UserRepository => new UserRepository(_context, _userManager); public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper); public async Task Complete() diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 72bcdcc63..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; } @@ -77,13 +73,5 @@ namespace API.Data .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/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index 8097c5abf..0f05f4dee 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; - + namespace API.Entities { /// diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 9c93fa337..3c2129c37 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; using API.Entities.Interfaces; namespace API.Entities diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 586dc04f0..714871813 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -10,13 +10,6 @@ namespace API.Helpers public AutoMapperProfiles() { CreateMap(); - // .ForMember(dest => dest.Folders, - // opt => - // opt.MapFrom(src => src.Folders.Select(x => new FolderPath() - // { - // Path = x, - // //LibraryId = src.Id - // }).ToList())); CreateMap(); diff --git a/API/Interfaces/IDirectoryService.cs b/API/Interfaces/IDirectoryService.cs index cb007b1a5..93e7c1e7b 100644 --- a/API/Interfaces/IDirectoryService.cs +++ b/API/Interfaces/IDirectoryService.cs @@ -13,9 +13,6 @@ namespace API.Interfaces /// List of folder names IEnumerable ListDirectory(string rootPath); - - //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. @@ -24,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/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 1e72cef3c..a33fc18aa 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -13,7 +13,7 @@ namespace API.Interfaces Series GetSeriesByName(string name); Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId); Task> GetSeriesForLibraryIdAsync(int libraryId); - Task> GetVolumesDtoAsync(int seriesId, int userId = 0); + Task> GetVolumesDtoAsync(int seriesId, int userId); IEnumerable GetVolumes(int seriesId); Task GetSeriesDtoByIdAsync(int seriesId); diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index d652ee149..9c619408a 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -210,7 +210,16 @@ 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; diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index d33b82758..490f8b2b3 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -8,21 +8,19 @@ namespace API.Services { private readonly ICacheService _cacheService; private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; private readonly IDirectoryService _directoryService; public BackgroundJobServer Client => new BackgroundJobServer(); public TaskScheduler(ICacheService cacheService, ILogger logger, - IUnitOfWork unitOfWork, IDirectoryService directoryService) + IDirectoryService directoryService) { _cacheService = cacheService; _logger = logger; - _unitOfWork = unitOfWork; _directoryService = directoryService; _logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis."); RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily); - //RecurringJob.AddOrUpdate(() => scanService.ScanLibraries(), Cron.Daily); + RecurringJob.AddOrUpdate(() => directoryService.ScanLibraries(), Cron.Daily); } public void ScanLibrary(int libraryId, bool forceUpdate = false)