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.
This commit is contained in:
Joseph Milazzo 2021-01-02 12:21:36 -06:00
parent d632e53f18
commit 9168e12483
13 changed files with 622 additions and 49 deletions

View File

@ -78,6 +78,8 @@ namespace API.Controllers
user.LastActive = DateTime.Now; user.LastActive = DateTime.Now;
_userRepository.Update(user); _userRepository.Update(user);
await _userRepository.SaveAllAsync(); await _userRepository.SaveAllAsync();
_logger.LogInformation($"{user.UserName} logged in at {user.LastActive}");
return new UserDto return new UserDto
{ {

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
@ -35,6 +36,47 @@ namespace API.Controllers
_taskScheduler = taskScheduler; _taskScheduler = taskScheduler;
_seriesRepository = seriesRepository; _seriesRepository = seriesRepository;
} }
/// <summary>
/// Creates a new Library. Upon library creation, adds new library to all Admin accounts.
/// </summary>
/// <param name="createLibraryDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("create")]
public async Task<ActionResult> 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<Library>();
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.");
}
/// <summary> /// <summary>
/// Returns a list of directories for a given path. If path is empty, returns root drives. /// 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. // 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 // 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)); BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(library));
return Ok(); return Ok();
} }
@ -106,7 +149,6 @@ namespace API.Controllers
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId) public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId)
{ {
return Ok(await _seriesRepository.GetSeriesForLibraryIdAsync(libraryId)); return Ok(await _seriesRepository.GetSeriesForLibraryIdAsync(libraryId));
} }
} }
} }

View File

@ -22,47 +22,6 @@ namespace API.Controllers
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
} }
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("add-library")]
public async Task<ActionResult> 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<AppUser>() { user }
};
library.Folders = createLibraryDto.Folders.Select(x => new FolderPath
{
Path = x,
Library = library
}).ToList();
user.Libraries ??= new List<Library>(); // If user is null, then set it
user.Libraries.Add(library);
if (await _userRepository.SaveAllAsync())
{
return Ok();
}
return BadRequest("Not implemented");
}
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete-user")] [HttpDelete("delete-user")]
public async Task<ActionResult> DeleteUser(string username) public async Task<ActionResult> DeleteUser(string username)

View File

@ -5,7 +5,8 @@ namespace API.DTOs
public class VolumeDto public class VolumeDto
{ {
public int Id { get; set; } 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 string CoverImage { get; set; }
public ICollection<string> Files { get; set; } public ICollection<string> Files { get; set; }
} }

View File

@ -0,0 +1,512 @@
// <auto-generated />
using System;
using API.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("API.Entities.AppUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("IsAdmin")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastActive")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("API.Entities.AppUserRole", b =>
{
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LibraryId");
b.ToTable("FolderPath");
});
modelBuilder.Entity("API.Entities.Library", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CoverImage")
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Library");
});
modelBuilder.Entity("API.Entities.MangaFile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("FilePath")
.HasColumnType("TEXT");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("VolumeId");
b.ToTable("MangaFile");
});
modelBuilder.Entity("API.Entities.Series", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CoverImage")
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("OriginalName")
.HasColumnType("TEXT");
b.Property<string>("SortName")
.HasColumnType("TEXT");
b.Property<string>("Summary")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LibraryId");
b.ToTable("Series");
});
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SeriesId");
b.ToTable("Volume");
});
modelBuilder.Entity("AppUserLibrary", b =>
{
b.Property<int>("AppUsersId")
.HasColumnType("INTEGER");
b.Property<int>("LibrariesId")
.HasColumnType("INTEGER");
b.HasKey("AppUsersId", "LibrariesId");
b.HasIndex("LibrariesId");
b.ToTable("AppUserLibrary");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
{
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("API.Entities.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<int>", b =>
{
b.HasOne("API.Entities.AppRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Entities.AppRole", b =>
{
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.AppUser", b =>
{
b.Navigation("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
}
}
}

View File

@ -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<int>(
name: "Number",
table: "Volume",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AddColumn<string>(
name: "Name",
table: "Volume",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Name",
table: "Volume");
migrationBuilder.AlterColumn<string>(
name: "Number",
table: "Volume",
type: "TEXT",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
}
}
}

View File

@ -249,9 +249,12 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastModified") b.Property<DateTime>("LastModified")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Number") b.Property<string>("Name")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId") b.Property<int>("SeriesId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View File

@ -50,6 +50,7 @@ namespace API.Data
{ {
return await _context.Series return await _context.Series
.Where(series => series.LibraryId == libraryId) .Where(series => series.LibraryId == libraryId)
.OrderBy(s => s.SortName)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider).ToListAsync(); .ProjectTo<SeriesDto>(_mapper.ConfigurationProvider).ToListAsync();
} }
@ -57,6 +58,7 @@ namespace API.Data
{ {
return await _context.Volume return await _context.Volume
.Where(vol => vol.SeriesId == seriesId) .Where(vol => vol.SeriesId == seriesId)
.OrderBy(volume => volume.Number)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider).ToListAsync(); .ProjectTo<VolumeDto>(_mapper.ConfigurationProvider).ToListAsync();
} }
@ -64,6 +66,7 @@ namespace API.Data
{ {
return _context.Volume return _context.Volume
.Where(vol => vol.SeriesId == seriesId) .Where(vol => vol.SeriesId == seriesId)
.OrderBy(vol => vol.Number)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider).ToList(); .ProjectTo<VolumeDto>(_mapper.ConfigurationProvider).ToList();
} }
@ -71,6 +74,7 @@ namespace API.Data
{ {
return _context.Volume return _context.Volume
.Where(vol => vol.SeriesId == seriesId) .Where(vol => vol.SeriesId == seriesId)
.OrderBy(vol => vol.Number)
.ToList(); .ToList();
} }

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
using API.Interfaces; using API.Interfaces;
@ -55,6 +56,11 @@ namespace API.Data
.SingleOrDefaultAsync(x => x.UserName == username); .SingleOrDefaultAsync(x => x.UserName == username);
} }
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
}
public async Task<IEnumerable<MemberDto>> GetMembersAsync() public async Task<IEnumerable<MemberDto>> GetMembersAsync()
{ {
return await _userManager.Users return await _userManager.Users

View File

@ -1,16 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Entities.Interfaces;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace API.Entities namespace API.Entities
{ {
public class AppUser : IdentityUser<int> public class AppUser : IdentityUser<int>, IHasConcurrencyToken
{ {
public DateTime Created { get; set; } = DateTime.Now; public DateTime Created { get; set; } = DateTime.Now;
public DateTime LastActive { get; set; } public DateTime LastActive { get; set; }
public bool IsAdmin { get; set; }
public ICollection<Library> Libraries { get; set; } public ICollection<Library> Libraries { get; set; }
[ConcurrencyCheck] [ConcurrencyCheck]

View File

@ -7,7 +7,8 @@ namespace API.Entities
public class Volume : IEntityDate public class Volume : IEntityDate
{ {
public int Id { get; set; } public int Id { get; set; }
public string Number { get; set; } public string Name { get; set; }
public int Number { get; set; }
public ICollection<MangaFile> Files { get; set; } public ICollection<MangaFile> Files { get; set; }
public DateTime Created { get; set; } public DateTime Created { get; set; }
public DateTime LastModified { get; set; } public DateTime LastModified { get; set; }

View File

@ -15,5 +15,6 @@ namespace API.Interfaces
Task<IEnumerable<MemberDto>> GetMembersAsync(); Task<IEnumerable<MemberDto>> GetMembersAsync();
Task<MemberDto> GetMemberAsync(string username); Task<MemberDto> GetMemberAsync(string username);
public void Delete(AppUser user); public void Delete(AppUser user);
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
} }
} }

View File

@ -151,7 +151,7 @@ namespace API.Services
IList<Volume> existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList(); IList<Volume> existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList();
foreach (var info in infos) 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) if (existingVolume != null)
{ {
// Temp let's overwrite all files (we need to enhance to update files) // Temp let's overwrite all files (we need to enhance to update files)
@ -168,7 +168,8 @@ namespace API.Services
{ {
var vol = new Volume() var vol = new Volume()
{ {
Number = info.Volumes, Name = info.Volumes,
Number = Int32.Parse(info.Volumes),
Files = new List<MangaFile>() Files = new List<MangaFile>()
{ {
new MangaFile() new MangaFile()