diff --git a/.gitignore b/.gitignore
index 75589ba35..343c37b63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -445,4 +445,6 @@ $RECYCLE.BIN/
appsettings.json
/API/kavita.db
/API/kavita.db-shm
-/API/kavita.db-wal
\ No newline at end of file
+/API/kavita.db-wal
+/API/Hangfire.db
+/API/Hangfire-log.db
\ No newline at end of file
diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
new file mode 100644
index 000000000..e19d7abc9
--- /dev/null
+++ b/API.Tests/API.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs
new file mode 100644
index 000000000..82c4a4892
--- /dev/null
+++ b/API.Tests/ParserTest.cs
@@ -0,0 +1,86 @@
+using Xunit;
+using static API.Parser.Parser;
+
+namespace API.Tests
+{
+ public class ParserTests
+ {
+ [Theory]
+ [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
+ [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "1")]
+ [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98.zip", "11")]
+ [InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", "1")]
+ [InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", "1")]
+ [InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1")]
+ //[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "16-17")]
+ [InlineData("Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", "1")]
+ [InlineData("v001", "1")]
+ [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1")]
+ public void ParseVolumeTest(string filename, string expected)
+ {
+ var result = ParseVolume(filename);
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "Killing Bites")]
+ [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "My Girlfriend Is Shobitch")]
+ [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98.zip", "Historys Strongest Disciple Kenichi")]
+ [InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", "B Gata H Kei")]
+ [InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", "BTOOOM!")]
+ [InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "Gokukoku no Brynhildr")]
+ [InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "Dance in the Vampire Bund")]
+ [InlineData("v001", "")]
+ [InlineData("Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)", "Akame ga KILL! ZERO")]
+ public void ParseSeriesTest(string filename, string expected)
+ {
+ var result = ParseSeries(filename);
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
+ [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")]
+ [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98.zip", "90-98")]
+ [InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", "")]
+ [InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", "")]
+ [InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")]
+ [InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "")]
+ [InlineData("c001", "1")]
+ [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "12")]
+ public void ParseChaptersTest(string filename, string expected)
+ {
+ var result = ParseChapter(filename);
+ Assert.Equal(expected, result);
+ }
+
+
+ [Theory]
+ [InlineData("0001", "1")]
+ [InlineData("1", "1")]
+ [InlineData("0013", "13")]
+ public void RemoveLeadingZeroesTest(string input, string expected)
+ {
+ Assert.Equal(expected, RemoveLeadingZeroes(input));
+ }
+
+ [Theory]
+ [InlineData("1", "001")]
+ [InlineData("10", "010")]
+ [InlineData("100", "100")]
+ public void PadZerosTest(string input, string expected)
+ {
+ Assert.Equal(expected, PadZeros(input));
+ }
+
+ [Theory]
+ [InlineData("Hello_I_am_here", "Hello I am here")]
+ [InlineData("Hello_I_am_here ", "Hello I am here")]
+ [InlineData("[ReleaseGroup] The Title", "The Title")]
+ [InlineData("[ReleaseGroup]_The_Title", "The Title")]
+ public void CleanTitleTest(string input, string expected)
+ {
+ Assert.Equal(expected, CleanTitle(input));
+ }
+ }
+}
\ No newline at end of file
diff --git a/API/API.csproj b/API/API.csproj
index cdd2f9198..1b99ef72e 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs
index fa495b62e..4aba6b7bd 100644
--- a/API/Controllers/AdminController.cs
+++ b/API/Controllers/AdminController.cs
@@ -1,6 +1,5 @@
using System.Threading.Tasks;
using API.Entities;
-using API.Interfaces;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@@ -8,12 +7,10 @@ namespace API.Controllers
{
public class AdminController : BaseApiController
{
- private readonly IUserRepository _userRepository;
private readonly UserManager _userManager;
- public AdminController(IUserRepository userRepository, UserManager userManager)
+ public AdminController(UserManager userManager)
{
- _userRepository = userRepository;
_userManager = userManager;
}
diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs
index 82e88afb0..e06cba530 100644
--- a/API/Controllers/LibraryController.cs
+++ b/API/Controllers/LibraryController.cs
@@ -1,8 +1,6 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
-using API.Data;
using API.DTOs;
using API.Entities;
using API.Interfaces;
@@ -23,10 +21,11 @@ namespace API.Controllers
private readonly IUserRepository _userRepository;
private readonly IMapper _mapper;
private readonly ITaskScheduler _taskScheduler;
+ private readonly ISeriesRepository _seriesRepository;
public LibraryController(IDirectoryService directoryService,
ILibraryRepository libraryRepository, ILogger logger, IUserRepository userRepository,
- IMapper mapper, ITaskScheduler taskScheduler)
+ IMapper mapper, ITaskScheduler taskScheduler, ISeriesRepository seriesRepository)
{
_directoryService = directoryService;
_libraryRepository = libraryRepository;
@@ -34,6 +33,7 @@ namespace API.Controllers
_userRepository = userRepository;
_mapper = mapper;
_taskScheduler = taskScheduler;
+ _seriesRepository = seriesRepository;
}
///
@@ -81,18 +81,32 @@ namespace API.Controllers
return Ok(user);
}
- return BadRequest("Not Implemented");
+ return BadRequest("There was a critical issue. Please try again.");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("scan")]
public async Task ScanLibrary(int libraryId)
{
- var library = await _libraryRepository.GetLibraryForIdAsync(libraryId);
+ var library = await _libraryRepository.GetLibraryDtoForIdAsync(libraryId);
+ // 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
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(library));
-
return Ok();
}
+
+ [HttpGet("libraries-for")]
+ public async Task>> GetLibrariesForUser(string username)
+ {
+ return Ok(await _libraryRepository.GetLibrariesForUsernameAysnc(username));
+ }
+
+ [HttpGet("series")]
+ public async Task>> GetSeriesForLibrary(int libraryId)
+ {
+ return Ok(await _seriesRepository.GetSeriesForLibraryIdAsync(libraryId));
+
+ }
}
}
\ No newline at end of file
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
new file mode 100644
index 000000000..dbdfaf93e
--- /dev/null
+++ b/API/Controllers/SeriesController.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using API.DTOs;
+using API.Interfaces;
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace API.Controllers
+{
+ public class SeriesController : BaseApiController
+ {
+ private readonly ILogger _logger;
+ private readonly IMapper _mapper;
+ private readonly ITaskScheduler _taskScheduler;
+ private readonly ISeriesRepository _seriesRepository;
+
+ public SeriesController(ILogger logger, IMapper mapper,
+ ITaskScheduler taskScheduler, ISeriesRepository seriesRepository)
+ {
+ _logger = logger;
+ _mapper = mapper;
+ _taskScheduler = taskScheduler;
+ _seriesRepository = seriesRepository;
+ }
+
+ [HttpGet("{seriesId}")]
+ public async Task> GetSeries(int seriesId)
+ {
+ return Ok(await _seriesRepository.GetSeriesByIdAsync(seriesId));
+ }
+
+ [HttpGet("volumes")]
+ public async Task>> GetVolumes(int seriesId)
+ {
+ return Ok(await _seriesRepository.GetVolumesAsync(seriesId));
+ }
+ }
+}
\ No newline at end of file
diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs
index 6102cc5e5..db81af51d 100644
--- a/API/Controllers/UsersController.cs
+++ b/API/Controllers/UsersController.cs
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using API.Data;
using API.DTOs;
using API.Entities;
using API.Extensions;
@@ -23,6 +22,7 @@ namespace API.Controllers
_libraryRepository = libraryRepository;
}
+ [Authorize(Policy = "RequireAdminRole")]
[HttpPost("add-library")]
public async Task AddLibrary(CreateLibraryDto createLibraryDto)
{
@@ -38,7 +38,6 @@ namespace API.Controllers
return BadRequest("Library name already exists. Please choose a unique name to the server.");
}
- // TODO: We probably need to clean the folders before we insert
var library = new Library
{
Name = createLibraryDto.Name.ToLower(),
diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs
index 31e51e173..e23268269 100644
--- a/API/DTOs/LibraryDto.cs
+++ b/API/DTOs/LibraryDto.cs
@@ -5,6 +5,7 @@ namespace API.DTOs
{
public class LibraryDto
{
+ public int Id { get; init; }
public string Name { get; set; }
public string CoverImage { get; set; }
public LibraryType Type { get; set; }
diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs
new file mode 100644
index 000000000..9e9491d7e
--- /dev/null
+++ b/API/DTOs/SeriesDto.cs
@@ -0,0 +1,11 @@
+namespace API.DTOs
+{
+ 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; }
+ }
+}
\ No newline at end of file
diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs
new file mode 100644
index 000000000..1753d4679
--- /dev/null
+++ b/API/DTOs/VolumeDto.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace API.DTOs
+{
+ public class VolumeDto
+ {
+ public int Id { get; set; }
+ public string Number { get; set; }
+ public string CoverImage { get; set; }
+ public ICollection Files { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs
index 9772251b7..a83a76e9e 100644
--- a/API/Data/DataContext.cs
+++ b/API/Data/DataContext.cs
@@ -14,6 +14,9 @@ namespace API.Data
}
public DbSet Library { get; set; }
+ public DbSet Series { get; set; }
+ public DbSet Volume { get; set; }
+
protected override void OnModelCreating(ModelBuilder builder)
{
@@ -30,6 +33,12 @@ namespace API.Data
.WithOne(u => u.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
+
+ // builder.Entity()
+ // .HasMany(s => s.Series)
+ // .WithOne(l => l.Library)
+ // .HasForeignKey(x => x.Id)
+
}
}
}
\ No newline at end of file
diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs
index b7bd978a1..ce986279f 100644
--- a/API/Data/LibraryRepository.cs
+++ b/API/Data/LibraryRepository.cs
@@ -30,6 +30,28 @@ namespace API.Data
{
return await _context.SaveChangesAsync() > 0;
}
+
+ public bool SaveAll()
+ {
+ return _context.SaveChanges() > 0;
+ }
+
+ public Library GetLibraryForName(string libraryName)
+ {
+ return _context.Library
+ .Where(x => x.Name == libraryName)
+ .Include(f => f.Folders)
+ .Include(s => s.Series)
+ .Single();
+ }
+
+ public async Task> GetLibrariesForUsernameAysnc(string userName)
+ {
+ return await _context.Library
+ .Include(l => l.AppUsers)
+ .Where(library => library.AppUsers.Any(x => x.UserName == userName))
+ .ProjectTo(_mapper.ConfigurationProvider).ToListAsync();
+ }
public async Task> GetLibrariesAsync()
{
@@ -38,7 +60,7 @@ namespace API.Data
.ProjectTo(_mapper.ConfigurationProvider).ToListAsync();
}
- public async Task GetLibraryForIdAsync(int libraryId)
+ public async Task GetLibraryDtoForIdAsync(int libraryId)
{
return await _context.Library
.Where(x => x.Id == libraryId)
@@ -46,7 +68,14 @@ namespace API.Data
.ProjectTo(_mapper.ConfigurationProvider).SingleAsync();
}
-
+ public async Task GetLibraryForIdAsync(int libraryId)
+ {
+ return await _context.Library
+ .Where(x => x.Id == libraryId)
+ .Include(f => f.Folders)
+ .SingleAsync();
+ }
+
public async Task LibraryExists(string libraryName)
{
return await _context.Library.AnyAsync(x => x.Name == libraryName);
diff --git a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs b/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs
new file mode 100644
index 000000000..5cf25a225
--- /dev/null
+++ b/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs
@@ -0,0 +1,488 @@
+//
+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("20201229190216_SeriesAndVolumeEntities")]
+ partial class SeriesAndVolumeEntities
+ {
+ 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("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("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("Number")
+ .HasColumnType("TEXT");
+
+ 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/20201229190216_SeriesAndVolumeEntities.cs b/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.cs
new file mode 100644
index 000000000..5b4302ba3
--- /dev/null
+++ b/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.cs
@@ -0,0 +1,129 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace API.Data.Migrations
+{
+ public partial class SeriesAndVolumeEntities : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "PasswordSalt",
+ table: "AspNetUsers");
+
+ migrationBuilder.AlterColumn(
+ name: "PasswordHash",
+ table: "AspNetUsers",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(byte[]),
+ oldType: "BLOB",
+ oldNullable: true);
+
+ migrationBuilder.CreateTable(
+ name: "Series",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Name = table.Column(type: "TEXT", nullable: true),
+ OriginalName = table.Column(type: "TEXT", nullable: true),
+ SortName = table.Column(type: "TEXT", nullable: true),
+ Summary = table.Column(type: "TEXT", nullable: true),
+ LibraryId = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Series", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Series_Library_LibraryId",
+ column: x => x.LibraryId,
+ principalTable: "Library",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Volume",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Number = table.Column(type: "TEXT", nullable: true),
+ SeriesId = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Volume", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Volume_Series_SeriesId",
+ column: x => x.SeriesId,
+ principalTable: "Series",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MangaFile",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ FilePath = table.Column(type: "TEXT", nullable: true),
+ VolumeId = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MangaFile", x => x.Id);
+ table.ForeignKey(
+ name: "FK_MangaFile_Volume_VolumeId",
+ column: x => x.VolumeId,
+ principalTable: "Volume",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MangaFile_VolumeId",
+ table: "MangaFile",
+ column: "VolumeId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Series_LibraryId",
+ table: "Series",
+ column: "LibraryId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Volume_SeriesId",
+ table: "Volume",
+ column: "SeriesId");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "MangaFile");
+
+ migrationBuilder.DropTable(
+ name: "Volume");
+
+ migrationBuilder.DropTable(
+ name: "Series");
+
+ migrationBuilder.AlterColumn(
+ name: "PasswordHash",
+ table: "AspNetUsers",
+ type: "BLOB",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "PasswordSalt",
+ table: "AspNetUsers",
+ type: "BLOB",
+ nullable: true);
+ }
+ }
+}
diff --git a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs b/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs
new file mode 100644
index 000000000..a1a54360f
--- /dev/null
+++ b/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs
@@ -0,0 +1,491 @@
+//
+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("20210101180935_AddedCoverImageToSeries")]
+ partial class AddedCoverImageToSeries
+ {
+ 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("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("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("Number")
+ .HasColumnType("TEXT");
+
+ 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/20210101180935_AddedCoverImageToSeries.cs b/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.cs
new file mode 100644
index 000000000..45e0fdc41
--- /dev/null
+++ b/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.cs
@@ -0,0 +1,23 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace API.Data.Migrations
+{
+ public partial class AddedCoverImageToSeries : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "CoverImage",
+ table: "Series",
+ type: "TEXT",
+ nullable: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CoverImage",
+ table: "Series");
+ }
+ }
+}
diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs
index b7088bacf..110c60774 100644
--- a/API/Data/Migrations/DataContextModelSnapshot.cs
+++ b/API/Data/Migrations/DataContextModelSnapshot.cs
@@ -1,7 +1,9 @@
//
using System;
+using API.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace API.Data.Migrations
{
@@ -84,11 +86,8 @@ namespace API.Data.Migrations
.HasMaxLength(256)
.HasColumnType("TEXT");
- b.Property("PasswordHash")
- .HasColumnType("BLOB");
-
- b.Property("PasswordSalt")
- .HasColumnType("BLOB");
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
b.Property("PhoneNumber")
.HasColumnType("TEXT");
@@ -176,6 +175,75 @@ namespace API.Data.Migrations
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("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("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("Volume");
+ });
+
modelBuilder.Entity("AppUserLibrary", b =>
{
b.Property("AppUsersId")
@@ -305,6 +373,39 @@ namespace API.Data.Migrations
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)
@@ -369,6 +470,18 @@ namespace API.Data.Migrations
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/SeriesRepository.cs b/API/Data/SeriesRepository.cs
new file mode 100644
index 000000000..fd109f45f
--- /dev/null
+++ b/API/Data/SeriesRepository.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using API.DTOs;
+using API.Entities;
+using API.Interfaces;
+using AutoMapper;
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Data
+{
+ public class SeriesRepository : ISeriesRepository
+ {
+ private readonly DataContext _context;
+ private readonly IMapper _mapper;
+
+ public SeriesRepository(DataContext context, IMapper mapper)
+ {
+ _context = context;
+ _mapper = mapper;
+ }
+
+ public void Update(Series series)
+ {
+ _context.Entry(series).State = EntityState.Modified;
+ }
+
+ public async Task SaveAllAsync()
+ {
+ return await _context.SaveChangesAsync() > 0;
+ }
+
+ public bool SaveAll()
+ {
+ return _context.SaveChanges() > 0;
+ }
+
+ public async Task GetSeriesByNameAsync(string name)
+ {
+ return await _context.Series.SingleOrDefaultAsync(x => x.Name == name);
+ }
+
+ public Series GetSeriesByName(string name)
+ {
+ return _context.Series.SingleOrDefault(x => x.Name == name);
+ }
+
+ public async Task> GetSeriesForLibraryIdAsync(int libraryId)
+ {
+ return await _context.Series
+ .Where(series => series.LibraryId == libraryId)
+ .ProjectTo(_mapper.ConfigurationProvider).ToListAsync();
+ }
+
+ public async Task> GetVolumesAsync(int seriesId)
+ {
+ return await _context.Volume
+ .Where(vol => vol.SeriesId == seriesId)
+ .ProjectTo(_mapper.ConfigurationProvider).ToListAsync();
+ }
+
+ public IEnumerable GetVolumesDto(int seriesId)
+ {
+ return _context.Volume
+ .Where(vol => vol.SeriesId == seriesId)
+ .ProjectTo(_mapper.ConfigurationProvider).ToList();
+ }
+
+ public IEnumerable GetVolumes(int seriesId)
+ {
+ return _context.Volume
+ .Where(vol => vol.SeriesId == seriesId)
+ .ToList();
+ }
+
+ public async Task GetSeriesByIdAsync(int seriesId)
+ {
+ return await _context.Series.Where(x => x.Id == seriesId)
+ .ProjectTo(_mapper.ConfigurationProvider).SingleAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs
index 279498210..53b3073a7 100644
--- a/API/Entities/Library.cs
+++ b/API/Entities/Library.cs
@@ -10,5 +10,6 @@ namespace API.Entities
public LibraryType Type { get; set; }
public ICollection Folders { get; set; }
public ICollection AppUsers { get; set; }
+ public ICollection Series { get; set; }
}
}
\ No newline at end of file
diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs
new file mode 100644
index 000000000..685d399e9
--- /dev/null
+++ b/API/Entities/MangaFile.cs
@@ -0,0 +1,13 @@
+namespace API.Entities
+{
+ public class MangaFile
+ {
+ public int Id { get; set; }
+ public string FilePath { get; set; }
+
+ // Relationship Mapping
+ public Volume Volume { get; set; }
+ public int VolumeId { get; set; }
+
+ }
+}
\ No newline at end of file
diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs
new file mode 100644
index 000000000..dd47e528d
--- /dev/null
+++ b/API/Entities/Series.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+
+namespace API.Entities
+{
+ public class Series
+ {
+ public int Id { get; set; }
+ ///
+ /// The UI visible Name of the Series. This may or may not be the same as the OriginalName
+ ///
+ public string Name { get; set; }
+ ///
+ /// Original Japanese Name
+ ///
+ public string OriginalName { get; set; }
+ ///
+ /// The name used to sort the Series. By default, will be the same as Name.
+ ///
+ public string SortName { get; set; }
+ ///
+ /// Summary information related to the Series
+ ///
+ public string Summary { get; set; }
+ public string CoverImage { get; set; }
+ public ICollection Volumes { get; set; }
+
+ public Library Library { get; set; }
+ public int LibraryId { get; set; }
+
+
+ }
+}
\ No newline at end of file
diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs
new file mode 100644
index 000000000..bb3638323
--- /dev/null
+++ b/API/Entities/Volume.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace API.Entities
+{
+ public class Volume
+ {
+ public int Id { get; set; }
+ public string Number { get; set; }
+ public ICollection Files { get; set; }
+
+ // Many-to-Many relationships
+ public Series Series { get; set; }
+ public int SeriesId { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs
index 08077b5a1..2e0bb342c 100644
--- a/API/Extensions/ApplicationServiceExtensions.cs
+++ b/API/Extensions/ApplicationServiceExtensions.cs
@@ -19,6 +19,7 @@ namespace API.Extensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddDbContext(options =>
diff --git a/API/Hangfire-log.db b/API/Hangfire-log.db
deleted file mode 100644
index d8fc774c2..000000000
Binary files a/API/Hangfire-log.db and /dev/null differ
diff --git a/API/Hangfire.db b/API/Hangfire.db
deleted file mode 100644
index db5987848..000000000
Binary files a/API/Hangfire.db and /dev/null differ
diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs
index 08e16b68a..35d7bb951 100644
--- a/API/Helpers/AutoMapperProfiles.cs
+++ b/API/Helpers/AutoMapperProfiles.cs
@@ -10,6 +10,12 @@ namespace API.Helpers
public AutoMapperProfiles()
{
CreateMap();
+
+ CreateMap()
+ .ForMember(dest => dest.Files,
+ opt => opt.MapFrom(src => src.Files.Select(x => x.FilePath).ToList()));
+
+ CreateMap();
CreateMap()
.ForMember(dest => dest.Folders,
diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs
index ae2cf88d8..409068fc3 100644
--- a/API/Interfaces/ILibraryRepository.cs
+++ b/API/Interfaces/ILibraryRepository.cs
@@ -16,7 +16,10 @@ namespace API.Interfaces
///
///
Task LibraryExists(string libraryName);
-
- public Task GetLibraryForIdAsync(int libraryId);
+ Task GetLibraryDtoForIdAsync(int libraryId);
+ Task GetLibraryForIdAsync(int libraryId);
+ bool SaveAll();
+ Library GetLibraryForName(string libraryName);
+ Task> GetLibrariesForUsernameAysnc(string userName);
}
}
\ No newline at end of file
diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs
new file mode 100644
index 000000000..ddd085cec
--- /dev/null
+++ b/API/Interfaces/ISeriesRepository.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using API.DTOs;
+using API.Entities;
+
+namespace API.Interfaces
+{
+ public interface ISeriesRepository
+ {
+ void Update(Series series);
+ Task SaveAllAsync();
+ Task GetSeriesByNameAsync(string name);
+ Series GetSeriesByName(string name);
+ bool SaveAll();
+ Task> GetSeriesForLibraryIdAsync(int libraryId);
+ Task> GetVolumesAsync(int seriesId);
+ IEnumerable GetVolumesDto(int seriesId);
+ IEnumerable GetVolumes(int seriesId);
+ Task GetSeriesByIdAsync(int seriesId);
+
+ }
+}
\ No newline at end of file
diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs
new file mode 100644
index 000000000..f0633c36e
--- /dev/null
+++ b/API/Parser/Parser.cs
@@ -0,0 +1,230 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace API.Parser
+{
+ public static class Parser
+ {
+ public static readonly string MangaFileExtensions = @"\.cbz|\.cbr|\.png|\.jpeg|\.jpg|\.zip|\.rar";
+
+ //?: is a non-capturing group in C#, else anything in () will be a group
+ private static readonly Regex[] MangaVolumeRegex = new[]
+ {
+ // Historys Strongest Disciple Kenichi_v11_c90-98.zip
+ new Regex(
+
+ @"(?.*)(\b|_)v(?\d+)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ // Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
+ new Regex(
+ @"(vol. ?)(?0*[1-9]+)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ // Dance in the Vampire Bund v16-17
+ new Regex(
+
+ @"(?.*)(\b|_)v(?\d+-?\d+)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ new Regex(
+ @"(?:v)(?0*[1-9]+)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+
+ };
+
+ private static readonly Regex[] MangaSeriesRegex = new[]
+ {
+ // Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto]
+ new Regex(
+
+ @"(?.*)( - )(?:v|vo|c)\d",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ // Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
+ new Regex(
+
+ @"(?.*)(\b|_)v",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+
+ // Black Bullet
+ new Regex(
+
+ @"(?.*)(\b|_)(v|vo|c)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+
+ // Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)
+ new Regex(
+
+ @"(?.*)\(\d",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+
+ // [BAA]_Darker_than_Black_c1 (This is very greedy, make sure it's always last)
+ new Regex(
+ @"(?.*)(\b|_)(c)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ // Darker Than Black (This takes anything, we have to account for perfectly named folders)
+ new Regex(
+ @"(?.*)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+
+
+ };
+
+ private static readonly Regex[] ReleaseGroupRegex = new[]
+ {
+ // [TrinityBAKumA Finella&anon], [BAA]_, [SlowManga&OverloadScans], [batoto]
+ new Regex(@"(?:\[(?(?!\s).+?(?(?!\s).+?(?\d+-?\d*)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
+ new Regex(
+
+ @"v\d+\.(?