mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Merge pull request #20 from Kareadita/feature/cover-images
Implements ability to set cover images from archive files and to force updating DB entries. Cover images will be compressed and a thumbnail will be saved rather than raw image due to size of raw files (1MB vs 60 KB)
This commit is contained in:
commit
cf953c1e06
@ -23,4 +23,8 @@
|
||||
<ProjectReference Include="..\API\API.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Services\Test Data" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -82,5 +82,15 @@ namespace API.Tests
|
||||
{
|
||||
Assert.Equal(expected, CleanTitle(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("test.cbz", true)]
|
||||
[InlineData("test.cbr", true)]
|
||||
[InlineData("test.zip", true)]
|
||||
[InlineData("test.rar", true)]
|
||||
public void IsArchiveTest(string input, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IsArchive(input));
|
||||
}
|
||||
}
|
||||
}
|
24
API.Tests/Services/ImageProviderTest.cs
Normal file
24
API.Tests/Services/ImageProviderTest.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using API.IO;
|
||||
using NetVips;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace API.Tests.Services
|
||||
{
|
||||
public class ImageProviderTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("v10.cbz", "v10.expected.jpg")]
|
||||
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.jpg")]
|
||||
//[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")]
|
||||
public void GetCoverImageTest(string inputFile, string expectedOutputFile)
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageProvider");
|
||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||
|
||||
Assert.Equal(expectedBytes, ImageProvider.GetCoverImage(Path.Join(testDirectory, inputFile)));
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
API.Tests/Services/Test Data/ImageProvider/thumbnail.jpg
Normal file
BIN
API.Tests/Services/Test Data/ImageProvider/thumbnail.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 395 KiB |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 385 KiB |
BIN
API.Tests/Services/Test Data/ImageProvider/v10 - with folder.cbz
Normal file
BIN
API.Tests/Services/Test Data/ImageProvider/v10 - with folder.cbz
Normal file
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 344 KiB |
BIN
API.Tests/Services/Test Data/ImageProvider/v10.cbz
Normal file
BIN
API.Tests/Services/Test Data/ImageProvider/v10.cbz
Normal file
Binary file not shown.
BIN
API.Tests/Services/Test Data/ImageProvider/v10.expected.jpg
Normal file
BIN
API.Tests/Services/Test Data/ImageProvider/v10.expected.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 385 KiB |
@ -20,6 +20,8 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="NetVips" Version="1.2.4" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.10.5.1" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.16.0.25740">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@ -30,6 +32,20 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Hangfire-log.db" />
|
||||
<None Remove="obj\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Interfaces\IMetadataService.cs" />
|
||||
<Compile Remove="obj\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="obj\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="obj\**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -72,7 +72,7 @@ namespace API.Controllers
|
||||
if (await _userRepository.SaveAllAsync())
|
||||
{
|
||||
var createdLibrary = await _libraryRepository.GetLibraryForNameAsync(library.Name);
|
||||
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id));
|
||||
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id, false));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -129,9 +129,9 @@ namespace API.Controllers
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan")]
|
||||
public ActionResult ScanLibrary(int libraryId)
|
||||
public ActionResult Scan(int libraryId)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(libraryId));
|
||||
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(libraryId, true));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -7,5 +7,6 @@
|
||||
public string OriginalName { get; set; }
|
||||
public string SortName { get; set; }
|
||||
public string Summary { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ namespace API.DTOs
|
||||
public int Id { get; set; }
|
||||
public int Number { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string CoverImage { get; set; }
|
||||
public ICollection<string> Files { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
}
|
||||
}
|
509
API/Data/Migrations/20210103230812_SeriesCoverImage.Designer.cs
generated
Normal file
509
API/Data/Migrations/20210103230812_SeriesCoverImage.Designer.cs
generated
Normal file
@ -0,0 +1,509 @@
|
||||
// <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("20210103230812_SeriesCoverImage")]
|
||||
partial class SeriesCoverImage
|
||||
{
|
||||
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<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<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
32
API/Data/Migrations/20210103230812_SeriesCoverImage.cs
Normal file
32
API/Data/Migrations/20210103230812_SeriesCoverImage.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class SeriesCoverImage : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "CoverImage",
|
||||
table: "Series",
|
||||
type: "BLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "CoverImage",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "BLOB",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
512
API/Data/Migrations/20210104011624_VolumeCoverImage.Designer.cs
generated
Normal file
512
API/Data/Migrations/20210104011624_VolumeCoverImage.Designer.cs
generated
Normal 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("20210104011624_VolumeCoverImage")]
|
||||
partial class VolumeCoverImage
|
||||
{
|
||||
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<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<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
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<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
24
API/Data/Migrations/20210104011624_VolumeCoverImage.cs
Normal file
24
API/Data/Migrations/20210104011624_VolumeCoverImage.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class VolumeCoverImage : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "CoverImage",
|
||||
table: "Volume",
|
||||
type: "BLOB",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImage",
|
||||
table: "Volume");
|
||||
}
|
||||
}
|
||||
}
|
@ -203,8 +203,8 @@ namespace API.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
@ -240,6 +240,9 @@ namespace API.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -66,6 +66,7 @@ namespace API.Data
|
||||
{
|
||||
return _context.Volume
|
||||
.Where(vol => vol.SeriesId == seriesId)
|
||||
.Include(vol => vol.Files)
|
||||
.OrderBy(vol => vol.Number)
|
||||
.ToList();
|
||||
}
|
||||
|
@ -23,14 +23,11 @@ namespace API.Entities
|
||||
/// Summary information related to the Series
|
||||
/// </summary>
|
||||
public string Summary { get; set; }
|
||||
public string CoverImage { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
public ICollection<Volume> Volumes { get; set; }
|
||||
|
||||
public Library Library { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -12,8 +12,9 @@ namespace API.Entities
|
||||
public ICollection<MangaFile> Files { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
|
||||
// Many-to-Many relationships
|
||||
// Many-to-One relationships
|
||||
public Series Series { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
}
|
||||
|
@ -11,9 +11,7 @@ namespace API.Helpers
|
||||
{
|
||||
CreateMap<LibraryDto, Library>();
|
||||
|
||||
CreateMap<Volume, VolumeDto>()
|
||||
.ForMember(dest => dest.Files,
|
||||
opt => opt.MapFrom(src => src.Files.Select(x => x.FilePath).ToList()));
|
||||
CreateMap<Volume, VolumeDto>();
|
||||
|
||||
CreateMap<Series, SeriesDto>();
|
||||
|
||||
|
69
API/IO/ImageProvider.cs
Normal file
69
API/IO/ImageProvider.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using NetVips;
|
||||
|
||||
namespace API.IO
|
||||
{
|
||||
public static class ImageProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates byte array of cover image.
|
||||
/// Given a path to a compressed file (zip, rar, cbz, cbr, etc), will ensure the first image is returned unless
|
||||
/// a folder.extension exists in the root directory of the compressed file.
|
||||
/// </summary>
|
||||
/// <param name="filepath"></param>
|
||||
/// <param name="createThumbnail">Create a smaller variant of file extracted from archive. Archive images are usually 1MB each.</param>
|
||||
/// <returns></returns>
|
||||
public static byte[] GetCoverImage(string filepath, bool createThumbnail = false)
|
||||
{
|
||||
if (!File.Exists(filepath) || !Parser.Parser.IsArchive(filepath)) return Array.Empty<byte>();
|
||||
|
||||
using ZipArchive archive = ZipFile.OpenRead(filepath);
|
||||
if (archive.Entries.Count <= 0) return Array.Empty<byte>();
|
||||
|
||||
|
||||
|
||||
var folder = archive.Entries.SingleOrDefault(x => Path.GetFileNameWithoutExtension(x.Name).ToLower() == "folder");
|
||||
var entry = archive.Entries.OrderBy(x => x.FullName).ToList()[0];
|
||||
|
||||
if (folder != null)
|
||||
{
|
||||
entry = folder;
|
||||
}
|
||||
|
||||
if (entry.FullName.EndsWith(Path.PathSeparator))
|
||||
{
|
||||
// TODO: Implement nested directory support
|
||||
}
|
||||
|
||||
if (createThumbnail)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
var thumbnail = Image.ThumbnailStream(stream, 320);
|
||||
Console.WriteLine(thumbnail.ToString());
|
||||
return thumbnail.WriteToBuffer(".jpg");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("There was a critical error and prevented thumbnail generation.");
|
||||
}
|
||||
}
|
||||
|
||||
return ExtractEntryToImage(entry);
|
||||
}
|
||||
|
||||
private static byte[] ExtractEntryToImage(ZipArchiveEntry entry)
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
var data = ms.ToArray();
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,6 @@ namespace API.Interfaces
|
||||
{
|
||||
IEnumerable<string> ListDirectory(string rootPath);
|
||||
|
||||
void ScanLibrary(int libraryId);
|
||||
void ScanLibrary(int libraryId, bool forceUpdate);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace API.Parser
|
||||
@ -226,5 +227,12 @@ namespace API.Parser
|
||||
{
|
||||
return title.TrimStart(new[] { '0' });
|
||||
}
|
||||
|
||||
public static bool IsArchive(string filePath)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
return MangaFileExtensions.Contains(fileInfo.Extension);
|
||||
}
|
||||
}
|
||||
}
|
@ -12,5 +12,6 @@ namespace API.Parser
|
||||
// This can be multiple
|
||||
public string Volumes { get; set; }
|
||||
public string File { get; init; }
|
||||
public string FullFilePath { get; set; }
|
||||
}
|
||||
}
|
@ -10,7 +10,9 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Interfaces;
|
||||
using API.IO;
|
||||
using API.Parser;
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services
|
||||
@ -20,9 +22,12 @@ namespace API.Services
|
||||
private readonly ILogger<DirectoryService> _logger;
|
||||
private readonly ISeriesRepository _seriesRepository;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
|
||||
private ConcurrentDictionary<string, ConcurrentBag<ParserInfo>> _scannedSeries;
|
||||
|
||||
public DirectoryService(ILogger<DirectoryService> logger, ISeriesRepository seriesRepository, ILibraryRepository libraryRepository)
|
||||
public DirectoryService(ILogger<DirectoryService> logger,
|
||||
ISeriesRepository seriesRepository,
|
||||
ILibraryRepository libraryRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_seriesRepository = seriesRepository;
|
||||
@ -45,10 +50,7 @@ namespace API.Services
|
||||
.Where(file =>
|
||||
reSearchPattern.IsMatch(Path.GetExtension(file)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Lists out top-level folders for a given directory. Filters out System and Hidden folders.
|
||||
/// </summary>
|
||||
@ -69,69 +71,47 @@ namespace API.Services
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Processes files found during a library scan.
|
||||
/// Processes files found during a library scan. Generates a collection of series->volume->files for DB processing later.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="path">Path of a file</param>
|
||||
private void Process(string path)
|
||||
{
|
||||
// NOTE: In current implementation, this never runs. We can probably remove.
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
DirectoryInfo di = new DirectoryInfo(path);
|
||||
_logger.LogDebug($"Parsing directory {di.Name}");
|
||||
var fileName = Path.GetFileName(path);
|
||||
_logger.LogDebug($"Parsing file {fileName}");
|
||||
|
||||
var seriesName = Parser.Parser.ParseSeries(di.Name);
|
||||
if (string.IsNullOrEmpty(seriesName))
|
||||
var info = Parser.Parser.Parse(fileName);
|
||||
info.FullFilePath = path;
|
||||
if (info.Volumes == string.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConcurrentBag<ParserInfo> tempBag;
|
||||
ConcurrentBag<ParserInfo> newBag = new ConcurrentBag<ParserInfo>();
|
||||
if (_scannedSeries.TryGetValue(info.Series, out tempBag))
|
||||
{
|
||||
var existingInfos = tempBag.ToArray();
|
||||
foreach (var existingInfo in existingInfos)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't need ContainsKey, this is a race condition. We can replace with TryAdd instead
|
||||
if (!_scannedSeries.ContainsKey(seriesName))
|
||||
{
|
||||
_scannedSeries.TryAdd(seriesName, new ConcurrentBag<ParserInfo>());
|
||||
newBag.Add(existingInfo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
_logger.LogDebug($"Parsing file {fileName}");
|
||||
|
||||
var info = Parser.Parser.Parse(fileName);
|
||||
if (info.Volumes != string.Empty)
|
||||
{
|
||||
ConcurrentBag<ParserInfo> tempBag;
|
||||
ConcurrentBag<ParserInfo> newBag = new ConcurrentBag<ParserInfo>();
|
||||
if (_scannedSeries.TryGetValue(info.Series, out tempBag))
|
||||
{
|
||||
var existingInfos = tempBag.ToArray();
|
||||
foreach (var existingInfo in existingInfos)
|
||||
{
|
||||
newBag.Add(existingInfo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tempBag = new ConcurrentBag<ParserInfo>();
|
||||
}
|
||||
|
||||
newBag.Add(info);
|
||||
tempBag = new ConcurrentBag<ParserInfo>();
|
||||
}
|
||||
|
||||
if (!_scannedSeries.TryUpdate(info.Series, newBag, tempBag))
|
||||
{
|
||||
_scannedSeries.TryAdd(info.Series, newBag);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
newBag.Add(info);
|
||||
|
||||
if (!_scannedSeries.TryUpdate(info.Series, newBag, tempBag))
|
||||
{
|
||||
_scannedSeries.TryAdd(info.Series, newBag);
|
||||
}
|
||||
}
|
||||
|
||||
private Series UpdateSeries(string seriesName, ParserInfo[] infos)
|
||||
|
||||
private Series UpdateSeries(string seriesName, ParserInfo[] infos, bool forceUpdate)
|
||||
{
|
||||
var series = _seriesRepository.GetSeriesByName(seriesName);
|
||||
ICollection<Volume> volumes = new List<Volume>();
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
@ -140,14 +120,29 @@ namespace API.Services
|
||||
Name = seriesName,
|
||||
OriginalName = seriesName,
|
||||
SortName = seriesName,
|
||||
Summary = "",
|
||||
Summary = ""
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// BUG: This is creating new volume entries and not resetting each run.
|
||||
var volumes = UpdateVolumes(series, infos, forceUpdate);
|
||||
series.Volumes = volumes;
|
||||
// TODO: Instead of taking first entry, re-calculate without compression
|
||||
series.CoverImage = volumes.OrderBy(x => x.Number).FirstOrDefault()?.CoverImage;
|
||||
return series;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or Updates volumes for a given series
|
||||
/// </summary>
|
||||
/// <param name="series">Series wanting to be updated</param>
|
||||
/// <param name="infos">Parser info</param>
|
||||
/// <param name="forceUpdate">Forces metadata update (cover image) even if it's already been set.</param>
|
||||
/// <returns>Updated Volumes for given series</returns>
|
||||
private ICollection<Volume> UpdateVolumes(Series series, ParserInfo[] infos, bool forceUpdate)
|
||||
{
|
||||
ICollection<Volume> volumes = new List<Volume>();
|
||||
IList<Volume> existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList();
|
||||
//IList<Volume> existingVolumes = Task.Run(() => _seriesRepository.GetVolumesAsync(series.Id)).Result.ToList();
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
|
||||
@ -161,6 +156,11 @@ namespace API.Services
|
||||
FilePath = info.File
|
||||
}
|
||||
};
|
||||
|
||||
if (forceUpdate || existingVolume.CoverImage == null || existingVolumes.Count == 0)
|
||||
{
|
||||
existingVolume.CoverImage = ImageProvider.GetCoverImage(info.FullFilePath, true);
|
||||
}
|
||||
volumes.Add(existingVolume);
|
||||
}
|
||||
else
|
||||
@ -169,6 +169,7 @@ namespace API.Services
|
||||
{
|
||||
Name = info.Volumes,
|
||||
Number = Int32.Parse(info.Volumes),
|
||||
CoverImage = ImageProvider.GetCoverImage(info.FullFilePath, true),
|
||||
Files = new List<MangaFile>()
|
||||
{
|
||||
new MangaFile()
|
||||
@ -183,12 +184,10 @@ namespace API.Services
|
||||
Console.WriteLine($"Adding volume {volumes.Last().Number} with File: {info.File}");
|
||||
}
|
||||
|
||||
series.Volumes = volumes;
|
||||
|
||||
return series;
|
||||
return volumes;
|
||||
}
|
||||
|
||||
public void ScanLibrary(int libraryId)
|
||||
public void ScanLibrary(int libraryId, bool forceUpdate)
|
||||
{
|
||||
var library = Task.Run(() => _libraryRepository.GetLibraryForIdAsync(libraryId)).Result;
|
||||
_scannedSeries = new ConcurrentDictionary<string, ConcurrentBag<ParserInfo>>();
|
||||
@ -221,11 +220,13 @@ namespace API.Services
|
||||
library.Series = new List<Series>(); // Temp delete everything until we can mark items Unavailable
|
||||
foreach (var seriesKey in series.Keys)
|
||||
{
|
||||
var s = UpdateSeries(seriesKey, series[seriesKey].ToArray());
|
||||
var s = UpdateSeries(seriesKey, series[seriesKey].ToArray(), forceUpdate);
|
||||
_logger.LogInformation($"Created/Updated series {s.Name}");
|
||||
library.Series.Add(s);
|
||||
}
|
||||
|
||||
|
||||
|
||||
_libraryRepository.Update(library);
|
||||
|
||||
if (_libraryRepository.SaveAll())
|
||||
@ -236,13 +237,12 @@ namespace API.Services
|
||||
{
|
||||
_logger.LogError("There was a critical error that resulted in a failed scan. Please rescan.");
|
||||
}
|
||||
|
||||
|
||||
_scannedSeries = null;
|
||||
}
|
||||
|
||||
private static void TraverseTreeParallelForEach(string root, Action<string> action)
|
||||
{
|
||||
{
|
||||
//Count of files traversed and timer for diagnostic output
|
||||
int fileCount = 0;
|
||||
var sw = Stopwatch.StartNew();
|
||||
@ -335,6 +335,7 @@ namespace API.Services
|
||||
|
||||
// For diagnostic purposes.
|
||||
Console.WriteLine("Processed {0} files in {1} milliseconds", fileCount, sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ namespace API
|
||||
|
||||
|
||||
app.UseHangfireDashboard();
|
||||
backgroundJobs.Enqueue(() => Console.WriteLine("Hello world from Hangfire!"));
|
||||
//backgroundJobs.Enqueue(() => Console.WriteLine("Hello world from Hangfire!"));
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user