From 9168e1248397259eb425b70d7c967b8448c80dae Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 2 Jan 2021 12:21:36 -0600 Subject: [PATCH] Refactored Volume to have Name and Number (int) so that we can properly sort and still handle possible split volumes. Refactored ScanLibrary into Library controller and updated it so it adds the new library to all admins. --- API/Controllers/AccountController.cs | 2 + API/Controllers/LibraryController.cs | 44 +- API/Controllers/UsersController.cs | 41 -- API/DTOs/VolumeDto.cs | 3 +- ...102173326_VolumeNumberRefactor.Designer.cs | 512 ++++++++++++++++++ .../20210102173326_VolumeNumberRefactor.cs | 41 ++ .../Migrations/DataContextModelSnapshot.cs | 5 +- API/Data/SeriesRepository.cs | 4 + API/Data/UserRepository.cs | 6 + API/Entities/AppUser.cs | 4 +- API/Entities/Volume.cs | 3 +- API/Interfaces/IUserRepository.cs | 1 + API/Services/DirectoryService.cs | 5 +- 13 files changed, 622 insertions(+), 49 deletions(-) create mode 100644 API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs create mode 100644 API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 3c25974ef..54d304dd6 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -78,6 +78,8 @@ namespace API.Controllers user.LastActive = DateTime.Now; _userRepository.Update(user); await _userRepository.SaveAllAsync(); + + _logger.LogInformation($"{user.UserName} logged in at {user.LastActive}"); return new UserDto { diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index b87e75cbf..64a259503 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.Entities; @@ -35,6 +36,47 @@ namespace API.Controllers _taskScheduler = taskScheduler; _seriesRepository = seriesRepository; } + + /// + /// Creates a new Library. Upon library creation, adds new library to all Admin accounts. + /// + /// + /// + [Authorize(Policy = "RequireAdminRole")] + [HttpPost("create")] + public async Task AddLibrary(CreateLibraryDto createLibraryDto) + { + if (await _libraryRepository.LibraryExists(createLibraryDto.Name)) + { + return BadRequest("Library name already exists. Please choose a unique name to the server."); + } + + var admins = (await _userRepository.GetAdminUsersAsync()).ToList(); + + var library = new Library + { + Name = createLibraryDto.Name, + Type = createLibraryDto.Type, + AppUsers = admins, + Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList() + }; + + foreach (var admin in admins) + { + // If user is null, then set it + admin.Libraries ??= new List(); + admin.Libraries.Add(library); + } + + + if (await _userRepository.SaveAllAsync()) + { + //TODO: Enqueue scan library task + return Ok(); + } + + return BadRequest("There was a critical issue. Please try again."); + } /// /// Returns a list of directories for a given path. If path is empty, returns root drives. @@ -92,6 +134,7 @@ namespace API.Controllers // We have to send a json encoded Library (aka a DTO) to the Background Job thread. // Because we use EF, we have circular dependencies back to Library and it will crap out + // TODO: Refactor this to use libraryId and move Library call in method. BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(library)); return Ok(); } @@ -106,7 +149,6 @@ namespace API.Controllers public async Task>> GetSeriesForLibrary(int libraryId) { return Ok(await _seriesRepository.GetSeriesForLibraryIdAsync(libraryId)); - } } } \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index a332c88c9..b860316df 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -22,47 +22,6 @@ namespace API.Controllers _libraryRepository = libraryRepository; } - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("add-library")] - public async Task AddLibrary(CreateLibraryDto createLibraryDto) - { - // NOTE: I think we should move this into library controller because it gets added to all admins - - var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); - - if (user == null) return BadRequest("Could not validate user"); - - - if (await _libraryRepository.LibraryExists(createLibraryDto.Name)) - { - return BadRequest("Library name already exists. Please choose a unique name to the server."); - } - - var library = new Library - { - Name = createLibraryDto.Name.ToLower(), - Type = createLibraryDto.Type, - AppUsers = new List() { user } - }; - - library.Folders = createLibraryDto.Folders.Select(x => new FolderPath - { - Path = x, - Library = library - }).ToList(); - - user.Libraries ??= new List(); // If user is null, then set it - - user.Libraries.Add(library); - - if (await _userRepository.SaveAllAsync()) - { - return Ok(); - } - - return BadRequest("Not implemented"); - } - [Authorize(Policy = "RequireAdminRole")] [HttpDelete("delete-user")] public async Task DeleteUser(string username) diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 1753d4679..b3dab2834 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -5,7 +5,8 @@ namespace API.DTOs public class VolumeDto { public int Id { get; set; } - public string Number { get; set; } + public int Number { get; set; } + public string Name { get; set; } public string CoverImage { get; set; } public ICollection Files { get; set; } } diff --git a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs b/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs new file mode 100644 index 000000000..1102111fc --- /dev/null +++ b/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs @@ -0,0 +1,512 @@ +// +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("20210102173326_VolumeNumberRefactor")] + partial class VolumeNumberRefactor + { + 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("IsAdmin") + .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.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("FilePath") + .HasColumnType("TEXT"); + + 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("TEXT"); + + 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("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .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.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("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/20210102173326_VolumeNumberRefactor.cs b/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs new file mode 100644 index 000000000..21cc8d42c --- /dev/null +++ b/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class VolumeNumberRefactor : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Number", + table: "Volume", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Name", + table: "Volume", + type: "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Name", + table: "Volume"); + + migrationBuilder.AlterColumn( + name: "Number", + table: "Volume", + type: "TEXT", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index f840250c9..5b22f751d 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -249,9 +249,12 @@ namespace API.Data.Migrations b.Property("LastModified") .HasColumnType("TEXT"); - b.Property("Number") + b.Property("Name") .HasColumnType("TEXT"); + b.Property("Number") + .HasColumnType("INTEGER"); + b.Property("SeriesId") .HasColumnType("INTEGER"); diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index fd109f45f..a9623275b 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -50,6 +50,7 @@ namespace API.Data { return await _context.Series .Where(series => series.LibraryId == libraryId) + .OrderBy(s => s.SortName) .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } @@ -57,6 +58,7 @@ namespace API.Data { return await _context.Volume .Where(vol => vol.SeriesId == seriesId) + .OrderBy(volume => volume.Number) .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } @@ -64,6 +66,7 @@ namespace API.Data { return _context.Volume .Where(vol => vol.SeriesId == seriesId) + .OrderBy(vol => vol.Number) .ProjectTo(_mapper.ConfigurationProvider).ToList(); } @@ -71,6 +74,7 @@ namespace API.Data { return _context.Volume .Where(vol => vol.SeriesId == seriesId) + .OrderBy(vol => vol.Number) .ToList(); } diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 6c63d31b9..5098b156c 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.Constants; using API.DTOs; using API.Entities; using API.Interfaces; @@ -55,6 +56,11 @@ namespace API.Data .SingleOrDefaultAsync(x => x.UserName == username); } + public async Task> GetAdminUsersAsync() + { + return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); + } + public async Task> GetMembersAsync() { return await _userManager.Users diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index ea7ad0245..f5b16890d 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using API.Entities.Interfaces; using Microsoft.AspNetCore.Identity; namespace API.Entities { - public class AppUser : IdentityUser + public class AppUser : IdentityUser, IHasConcurrencyToken { public DateTime Created { get; set; } = DateTime.Now; public DateTime LastActive { get; set; } - public bool IsAdmin { get; set; } public ICollection Libraries { get; set; } [ConcurrencyCheck] diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 80d1f6573..18cadee8d 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -7,7 +7,8 @@ namespace API.Entities public class Volume : IEntityDate { public int Id { get; set; } - public string Number { get; set; } + public string Name { get; set; } + public int Number { get; set; } public ICollection Files { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 31be6e52e..26e3b5a11 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -15,5 +15,6 @@ namespace API.Interfaces Task> GetMembersAsync(); Task GetMemberAsync(string username); public void Delete(AppUser user); + Task> GetAdminUsersAsync(); } } \ No newline at end of file diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index f8ee643ce..9d40069e7 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -151,7 +151,7 @@ namespace API.Services IList existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList(); foreach (var info in infos) { - var existingVolume = existingVolumes.SingleOrDefault(v => v.Number == info.Volumes); + var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes); if (existingVolume != null) { // Temp let's overwrite all files (we need to enhance to update files) @@ -168,7 +168,8 @@ namespace API.Services { var vol = new Volume() { - Number = info.Volumes, + Name = info.Volumes, + Number = Int32.Parse(info.Volumes), Files = new List() { new MangaFile()