diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 22ba556ce..e65e0e2dd 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -86,8 +86,10 @@ namespace API.Controllers [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(); 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..3f77a0e15 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -30,6 +30,20 @@ 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> GetLibrariesAsync() { @@ -38,7 +52,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 +60,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/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index b7088bacf..d1d3553a3 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,72 @@ 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("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 +370,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 +467,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..b00239beb --- /dev/null +++ b/API/Data/SeriesRepository.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading.Tasks; +using API.Entities; +using API.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace API.Data +{ + public class SeriesRepository : ISeriesRepository + { + private readonly DataContext _context; + + public SeriesRepository(DataContext context) + { + _context = context; + } + + 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); + } + } +} \ 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..6818bbff7 --- /dev/null +++ b/API/Entities/MangaFile.cs @@ -0,0 +1,14 @@ +namespace API.Entities +{ + public class MangaFile + { + public int Id { get; set; } + public string FilePath { get; set; } + //public string FileExtension { 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 index 7545fccf7..3c475ecdd 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -4,6 +4,7 @@ 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 /// @@ -20,9 +21,11 @@ namespace API.Entities /// Summary information related to the Series /// public string Summary { 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 index 54d56804c..2cc36453d 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -4,8 +4,9 @@ namespace API.Entities { public class Volume { + public int Id { get; set; } public string Number { get; set; } - public ICollection Files { get; set; } + public ICollection Files { get; set; } // Many-to-Many relationships public Series Series { get; set; } 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/Interfaces/IDirectoryService.cs b/API/Interfaces/IDirectoryService.cs index 818aa9451..dcb1e763d 100644 --- a/API/Interfaces/IDirectoryService.cs +++ b/API/Interfaces/IDirectoryService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using API.DTOs; +using API.Entities; namespace API.Interfaces { diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index ae2cf88d8..112a6b7d8 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -16,7 +16,9 @@ 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); } } \ No newline at end of file diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs new file mode 100644 index 000000000..2853b98c1 --- /dev/null +++ b/API/Interfaces/ISeriesRepository.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +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(); + } +} \ No newline at end of file diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 81dfc1dc2..df9dda0be 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -5,13 +5,16 @@ using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Security; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using API.DTOs; +using API.Entities; using API.Interfaces; using API.Parser; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace API.Services @@ -19,11 +22,15 @@ namespace API.Services public class DirectoryService : IDirectoryService { private readonly ILogger _logger; + private readonly ISeriesRepository _seriesRepository; + private readonly ILibraryRepository _libraryRepository; private ConcurrentDictionary> _scannedSeries; - public DirectoryService(ILogger logger) + public DirectoryService(ILogger logger, ISeriesRepository seriesRepository, ILibraryRepository libraryRepository) { _logger = logger; + _seriesRepository = seriesRepository; + _libraryRepository = libraryRepository; } /// @@ -125,6 +132,42 @@ namespace API.Services } } + private Series UpdateSeries(string seriesName, ParserInfo[] infos) + { + var series = _seriesRepository.GetSeriesByName(seriesName); + + if (series == null) + { + series = new Series() + { + Name = seriesName, + OriginalName = seriesName, + SortName = seriesName, + Summary = "", + }; + } + + ICollection volumes = new List(); + foreach (var info in infos) + { + volumes.Add(new Volume() + { + Number = info.Volumes, + Files = new List() {new MangaFile() + { + FilePath = info.File + }} + }); + } + + series.Volumes = volumes; + + + //_seriesRepository.Update(series); + + return series; + } + public void ScanLibrary(LibraryDto library) { _scannedSeries = new ConcurrentDictionary>(); @@ -154,10 +197,62 @@ namespace API.Services var filtered = _scannedSeries.Where(kvp => !kvp.Value.IsEmpty); var series = filtered.ToImmutableDictionary(v => v.Key, v => v.Value); - // TODO: Perform DB activities on ImmutableDictionary + // Perform DB activities on ImmutableDictionary + var libraryEntity = _libraryRepository.GetLibraryForName(library.Name); + libraryEntity.Series = new List(); // Temp delete everything for testing + foreach (var seriesKey in series.Keys) + { + var s = UpdateSeries(seriesKey, series[seriesKey].ToArray()); + Console.WriteLine($"Created/Updated series {s.Name}"); + libraryEntity.Series.Add(s); + } + _libraryRepository.Update(libraryEntity); + + // This is throwing a DbUpdateConcurrencyException due to multiple threads modifying Library at one time. + try + { + if (_libraryRepository.SaveAll()) + { + _logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series."); + } + else + { + _logger.LogError("There was a critical error that resulted in a failed scan. Please rescan."); + } + } + catch (DbUpdateConcurrencyException ex) + { + foreach (var entry in ex.Entries) + { + if (entry.Entity is Series) + { + var proposedValues = entry.CurrentValues; + var databaseValues = entry.GetDatabaseValues(); + + foreach (var property in proposedValues.Properties) + { + var proposedValue = proposedValues[property]; + var databaseValue = databaseValues[property]; + + // TODO: decide which value should be written to database + // proposedValues[property] = ; + Console.WriteLine($"Proposed ({proposedValue}) vs Database ({databaseValue})"); + } + + // Refresh original values to bypass next concurrency check + entry.OriginalValues.SetValues(databaseValues); + } + else + { + throw new NotSupportedException( + "Don't know how to handle concurrency conflicts for " + + entry.Metadata.Name); + } + } + } - _logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series."); + _scannedSeries = null; }