Refactored DB to be Series->Volumes->Chapters instead. All functionality that previously worked still works. Cleanup still needed.

This commit is contained in:
Joseph Milazzo 2021-02-01 09:29:11 -06:00
parent a42e54a078
commit 53e85317f9
33 changed files with 2084 additions and 142 deletions

1
.gitignore vendored
View File

@ -450,3 +450,4 @@ appsettings.json
/API/Hangfire-log.db
cache/
/API/wwwroot/
/API/cache/

View File

@ -62,37 +62,37 @@ namespace API.Tests.Services
[Fact]
public void GetOrderedChaptersTest()
{
var files = new List<MangaFile>()
{
new()
{
Chapter = 1
},
new()
{
Chapter = 2
},
new()
{
Chapter = 0
},
};
var expected = new List<MangaFile>()
{
new()
{
Chapter = 1
},
new()
{
Chapter = 2
},
new()
{
Chapter = 0
},
};
Assert.NotStrictEqual(expected, _cacheService.GetOrderedChapters(files));
// var files = new List<Chapter>()
// {
// new()
// {
// Number = "1"
// },
// new()
// {
// Chapter = 2
// },
// new()
// {
// Chapter = 0
// },
// };
// var expected = new List<MangaFile>()
// {
// new()
// {
// Chapter = 1
// },
// new()
// {
// Chapter = 2
// },
// new()
// {
// Chapter = 0
// },
// };
// Assert.NotStrictEqual(expected, _cacheService.GetOrderedChapters(files));
}

View File

@ -31,27 +31,29 @@ namespace API.Controllers
}
[HttpGet("image")]
public async Task<ActionResult<ImageDto>> GetImage(int volumeId, int page)
public async Task<ActionResult<ImageDto>> GetImage(int chapterId, int page)
{
// Temp let's iterate the directory each call to get next image
var volume = await _cacheService.Ensure(volumeId);
var chapter = await _cacheService.Ensure(chapterId);
var (path, mangaFile) = _cacheService.GetCachedPagePath(volume, page);
if (chapter == null) return BadRequest("There was an issue finding image file for reading.");
var (path, mangaFile) = await _cacheService.GetCachedPagePath(chapter, page);
if (string.IsNullOrEmpty(path)) return BadRequest($"No such image for page {page}");
var file = await _directoryService.ReadImageAsync(path);
file.Page = page;
file.Chapter = mangaFile.Chapter;
//file.Chapter = chapter.Number;
file.MangaFileName = mangaFile.FilePath;
return Ok(file);
}
[HttpGet("get-bookmark")]
public async Task<ActionResult<int>> GetBookmark(int volumeId)
public async Task<ActionResult<int>> GetBookmark(int chapterId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user.Progresses == null) return Ok(0);
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.VolumeId == volumeId);
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId);
if (progress != null) return Ok(progress.PagesRead);
@ -62,12 +64,12 @@ namespace API.Controllers
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
_logger.LogInformation($"Saving {user.UserName} progress for {bookmarkDto.VolumeId} to page {bookmarkDto.PageNum}");
_logger.LogInformation($"Saving {user.UserName} progress for Chapter {bookmarkDto.ChapterId} to page {bookmarkDto.PageNum}");
// TODO: Don't let user bookmark past total pages.
user.Progresses ??= new List<AppUserProgress>();
var userProgress = user.Progresses.SingleOrDefault(x => x.VolumeId == bookmarkDto.VolumeId && x.AppUserId == user.Id);
var userProgress = user.Progresses.SingleOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id);
if (userProgress == null)
{
@ -77,13 +79,14 @@ namespace API.Controllers
PagesRead = bookmarkDto.PageNum,
VolumeId = bookmarkDto.VolumeId,
SeriesId = bookmarkDto.SeriesId,
ChapterId = bookmarkDto.ChapterId
});
}
else
{
userProgress.PagesRead = bookmarkDto.PageNum;
userProgress.SeriesId = bookmarkDto.SeriesId;
userProgress.VolumeId = bookmarkDto.VolumeId;
}
_unitOfWork.UserRepository.Update(user);
@ -92,8 +95,7 @@ namespace API.Controllers
{
return Ok();
}
return BadRequest("Could not save progress");
}
}

View File

@ -47,6 +47,11 @@ namespace API.Controllers
return Ok(result);
}
/// <summary>
/// Returns All volumes for a series with progress information and Chapters
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("volumes")]
public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId)
{
@ -61,13 +66,12 @@ namespace API.Controllers
return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, user.Id));
}
// [HttpGet("volume-files")]
// public async Task<ActionResult<IEnumerable<MangaFileDto>>> GetMangaFiles(int volumeId)
// {
// var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
// return Ok(await _unitOfWork.SeriesRepository.GetVolumeMangaFileDtos(volumeId));
// }
[HttpGet("chapter")]
public async Task<ActionResult<VolumeDto>> GetChapter(int chapterId)
{
return Ok(await _unitOfWork.VolumeRepository.GetChapterDtoAsync(chapterId));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan")]
public ActionResult Scan(int libraryId, int seriesId)

31
API/DTOs/ChapterDto.cs Normal file
View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
namespace API.DTOs
{
public class ChapterDto
{
public int Id { get; set; }
/// <summary>
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2".
/// </summary>
public string Range { get; set; }
/// <summary>
/// Smallest number of the Range.
/// </summary>
public string Number { get; set; }
public byte[] CoverImage { get; set; }
/// <summary>
/// Total number of pages in all MangaFiles
/// </summary>
public int Pages { get; set; }
/// <summary>
/// The files that represent this Chapter
/// </summary>
public ICollection<MangaFileDto> Files { get; set; }
/// <summary>
/// Calculated at API time. Number of pages read for this Chapter for logged in user.
/// </summary>
public int PagesRead { get; set; }
public int VolumeId { get; set; }
}
}

View File

@ -5,7 +5,6 @@ namespace API.DTOs
public class MangaFileDto
{
public string FilePath { get; set; }
public int Chapter { get; set; }
public int NumberOfPages { get; set; }
public MangaFormat Format { get; set; }

View File

@ -11,6 +11,6 @@ namespace API.DTOs
public byte[] CoverImage { get; set; }
public int Pages { get; set; }
public int PagesRead { get; set; }
public ICollection<MangaFileDto> Files { get; set; }
public ICollection<ChapterDto> Chapters { get; set; }
}
}

View File

@ -3,6 +3,7 @@
public class BookmarkDto
{
public int VolumeId { get; init; }
public int ChapterId { get; init; }
public int PageNum { get; init; }
public int SeriesId { get; init; }
}

View File

@ -20,8 +20,11 @@ namespace API.Data
}
public DbSet<Library> Library { get; set; }
public DbSet<Series> Series { get; set; }
public DbSet<Chapter> Chapter { get; set; }
public DbSet<Volume> Volume { get; set; }
public DbSet<AppUser> AppUser { get; set; }
public DbSet<MangaFile> MangaFile { get; set; }
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
public DbSet<AppUserRating> AppUserRating { get; set; }
public DbSet<ServerSetting> ServerSetting { get; set; }

View File

@ -0,0 +1,688 @@
// <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("20210128143348_SeriesVolumeChapterChange")]
partial class SeriesVolumeChapterChange
{
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.AppUserProgress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("PagesRead")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserProgresses");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<string>("Review")
.HasColumnType("TEXT");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserRating");
});
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.Chapter", 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>("Number")
.HasColumnType("TEXT");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<string>("Range")
.HasColumnType("TEXT");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("VolumeId");
b.ToTable("Chapter");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastScanned")
.HasColumnType("TEXT");
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<int>("Chapter")
.HasColumnType("INTEGER");
b.Property<int?>("ChapterId")
.HasColumnType("INTEGER");
b.Property<string>("FilePath")
.HasColumnType("TEXT");
b.Property<int>("Format")
.HasColumnType("INTEGER");
b.Property<int>("NumberOfPages")
.HasColumnType("INTEGER");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId");
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<int>("Pages")
.HasColumnType("INTEGER");
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.ServerSetting", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("ServerSetting");
});
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<bool>("IsSpecial")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("Pages")
.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.AppUserProgress", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("Progresses")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("Ratings")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
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.Chapter", b =>
{
b.HasOne("API.Entities.Volume", "Volume")
.WithMany("Chapters")
.HasForeignKey("VolumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Volume");
});
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.Chapter", null)
.WithMany("Files")
.HasForeignKey("ChapterId");
b.HasOne("API.Entities.Volume", "Volume")
.WithMany()
.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("Progresses");
b.Navigation("Ratings");
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Navigation("Files");
});
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("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,111 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Data.Migrations
{
public partial class SeriesVolumeChapterChange : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsSpecial",
table: "Volume",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "ChapterId",
table: "MangaFile",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "LastScanned",
table: "FolderPath",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<int>(
name: "ChapterId",
table: "AppUserProgresses",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "Chapter",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Range = table.Column<string>(type: "TEXT", nullable: true),
Number = table.Column<string>(type: "TEXT", nullable: true),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
CoverImage = table.Column<byte[]>(type: "BLOB", nullable: true),
Pages = table.Column<int>(type: "INTEGER", nullable: false),
VolumeId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Chapter", x => x.Id);
table.ForeignKey(
name: "FK_Chapter_Volume_VolumeId",
column: x => x.VolumeId,
principalTable: "Volume",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MangaFile_ChapterId",
table: "MangaFile",
column: "ChapterId");
migrationBuilder.CreateIndex(
name: "IX_Chapter_VolumeId",
table: "Chapter",
column: "VolumeId");
migrationBuilder.AddForeignKey(
name: "FK_MangaFile_Chapter_ChapterId",
table: "MangaFile",
column: "ChapterId",
principalTable: "Chapter",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MangaFile_Chapter_ChapterId",
table: "MangaFile");
migrationBuilder.DropTable(
name: "Chapter");
migrationBuilder.DropIndex(
name: "IX_MangaFile_ChapterId",
table: "MangaFile");
migrationBuilder.DropColumn(
name: "IsSpecial",
table: "Volume");
migrationBuilder.DropColumn(
name: "ChapterId",
table: "MangaFile");
migrationBuilder.DropColumn(
name: "LastScanned",
table: "FolderPath");
migrationBuilder.DropColumn(
name: "ChapterId",
table: "AppUserProgresses");
}
}
}

View File

@ -0,0 +1,676 @@
// <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("20210128201832_MangaFileChapterRelationship")]
partial class MangaFileChapterRelationship
{
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.AppUserProgress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("PagesRead")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserProgresses");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("Rating")
.HasColumnType("INTEGER");
b.Property<string>("Review")
.HasColumnType("TEXT");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserRating");
});
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.Chapter", 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>("Number")
.HasColumnType("TEXT");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<string>("Range")
.HasColumnType("TEXT");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("VolumeId");
b.ToTable("Chapter");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastScanned")
.HasColumnType("TEXT");
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<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<string>("FilePath")
.HasColumnType("TEXT");
b.Property<int>("Format")
.HasColumnType("INTEGER");
b.Property<int>("NumberOfPages")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChapterId");
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<int>("Pages")
.HasColumnType("INTEGER");
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.ServerSetting", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("ServerSetting");
});
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<bool>("IsSpecial")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("Pages")
.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.AppUserProgress", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("Progresses")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("Ratings")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
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.Chapter", b =>
{
b.HasOne("API.Entities.Volume", "Volume")
.WithMany("Chapters")
.HasForeignKey("VolumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Volume");
});
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.Chapter", "Chapter")
.WithMany("Files")
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
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("Progresses");
b.Navigation("Ratings");
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Navigation("Files");
});
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("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,98 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace API.Data.Migrations
{
public partial class MangaFileChapterRelationship : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MangaFile_Chapter_ChapterId",
table: "MangaFile");
migrationBuilder.DropForeignKey(
name: "FK_MangaFile_Volume_VolumeId",
table: "MangaFile");
migrationBuilder.DropIndex(
name: "IX_MangaFile_VolumeId",
table: "MangaFile");
migrationBuilder.DropColumn(
name: "Chapter",
table: "MangaFile");
migrationBuilder.DropColumn(
name: "VolumeId",
table: "MangaFile");
migrationBuilder.AlterColumn<int>(
name: "ChapterId",
table: "MangaFile",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_MangaFile_Chapter_ChapterId",
table: "MangaFile",
column: "ChapterId",
principalTable: "Chapter",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MangaFile_Chapter_ChapterId",
table: "MangaFile");
migrationBuilder.AlterColumn<int>(
name: "ChapterId",
table: "MangaFile",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AddColumn<int>(
name: "Chapter",
table: "MangaFile",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "VolumeId",
table: "MangaFile",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_MangaFile_VolumeId",
table: "MangaFile",
column: "VolumeId");
migrationBuilder.AddForeignKey(
name: "FK_MangaFile_Chapter_ChapterId",
table: "MangaFile",
column: "ChapterId",
principalTable: "Chapter",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_MangaFile_Volume_VolumeId",
table: "MangaFile",
column: "VolumeId",
principalTable: "Volume",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -127,6 +127,9 @@ namespace API.Data.Migrations
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("PagesRead")
.HasColumnType("INTEGER");
@ -183,12 +186,49 @@ namespace API.Data.Migrations
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("API.Entities.Chapter", 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>("Number")
.HasColumnType("TEXT");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
b.Property<string>("Range")
.HasColumnType("TEXT");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("VolumeId");
b.ToTable("Chapter");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastScanned")
.HasColumnType("TEXT");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
@ -234,7 +274,7 @@ namespace API.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Chapter")
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<string>("FilePath")
@ -246,12 +286,9 @@ namespace API.Data.Migrations
b.Property<int>("NumberOfPages")
.HasColumnType("INTEGER");
b.Property<int>("VolumeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("VolumeId");
b.HasIndex("ChapterId");
b.ToTable("MangaFile");
});
@ -325,6 +362,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<bool>("IsSpecial")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
@ -487,6 +527,17 @@ namespace API.Data.Migrations
b.Navigation("User");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.HasOne("API.Entities.Volume", "Volume")
.WithMany("Chapters")
.HasForeignKey("VolumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Volume");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.HasOne("API.Entities.Library", "Library")
@ -500,13 +551,13 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.MangaFile", b =>
{
b.HasOne("API.Entities.Volume", "Volume")
b.HasOne("API.Entities.Chapter", "Chapter")
.WithMany("Files")
.HasForeignKey("VolumeId")
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Volume");
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Entities.Series", b =>
@ -596,6 +647,11 @@ namespace API.Data.Migrations
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Navigation("Files");
});
modelBuilder.Entity("API.Entities.Library", b =>
{
b.Navigation("Folders");
@ -610,7 +666,7 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.Navigation("Files");
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}

View File

@ -82,6 +82,7 @@ namespace API.Data
{
var volumes = await _context.Volume
.Where(vol => vol.SeriesId == seriesId)
.Include(vol => vol.Chapters)
.OrderBy(volume => volume.Number)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
@ -98,7 +99,8 @@ namespace API.Data
{
return _context.Volume
.Where(vol => vol.SeriesId == seriesId)
.Include(vol => vol.Files)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.OrderBy(vol => vol.Number)
.ToList();
}
@ -118,7 +120,8 @@ namespace API.Data
public async Task<Volume> GetVolumeAsync(int volumeId)
{
return await _context.Volume
.Include(vol => vol.Files)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
}
@ -126,14 +129,15 @@ namespace API.Data
{
var volume = await _context.Volume
.Where(vol => vol.Id == volumeId)
.Include(vol => vol.Files)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Files)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.SingleAsync(vol => vol.Id == volumeId);
var volumeList = new List<VolumeDto>() {volume};
await AddVolumeModifiers(userId, volumeList);
volumeList[0].Files = volumeList[0].Files.OrderBy(f => f.Chapter).ToList();
//TODO: volumeList[0].Files = volumeList[0].Files.OrderBy(f => f.Chapter).ToList();
return volumeList[0];
}
@ -199,6 +203,11 @@ namespace API.Data
foreach (var v in volumes)
{
foreach (var c in v.Chapters)
{
c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead);
}
v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);
}
}

View File

@ -22,6 +22,8 @@ namespace API.Data
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
public IUserRepository UserRepository => new UserRepository(_context, _userManager);
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
public async Task<bool> Complete()
{

View File

@ -0,0 +1,61 @@
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 VolumeRepository : IVolumeRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public VolumeRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(Volume volume)
{
_context.Entry(volume).State = EntityState.Modified;
}
/// <summary>
/// Returns a Chapter for an Id. Includes linked <see cref="MangaFile"/>s.
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public async Task<Chapter> GetChapterAsync(int chapterId)
{
return await _context.Chapter
.Include(c => c.Files)
.AsNoTracking()
.SingleOrDefaultAsync(c => c.Id == chapterId);
}
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId)
{
var chapter = await _context.Chapter
.Include(c => c.Files)
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.SingleOrDefaultAsync(c => c.Id == chapterId);
return chapter;
}
public async Task<IList<MangaFile>> GetFilesForChapter(int chapterId)
{
return await _context.MangaFile
.Where(c => chapterId == c.Id)
.AsNoTracking()
.ToListAsync();
}
}
}

View File

@ -2,7 +2,7 @@
namespace API.Entities
{
/// <summary>
/// Represents the progress a single user has on a given Volume.
/// Represents the progress a single user has on a given Volume. Progress is realistically tracked against the Volume's chapters.
/// </summary>
public class AppUserProgress
{
@ -11,6 +11,8 @@ namespace API.Entities
public int VolumeId { get; set; }
public int SeriesId { get; set; }
public int ChapterId { get; set; }
// Relationships
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }

35
API/Entities/Chapter.cs Normal file
View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using API.Entities.Interfaces;
namespace API.Entities
{
public class Chapter : IEntityDate
{
public int Id { get; set; }
/// <summary>
/// Range of numbers. Chapter 2-4 -> "2-4". Chapter 2 -> "2".
/// </summary>
public string Range { get; set; }
/// <summary>
/// Smallest number of the Range. Can be a partial like Chapter 4.5
/// </summary>
public string Number { get; set; }
/// <summary>
/// The files that represent this Chapter
/// </summary>
public ICollection<MangaFile> Files { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public byte[] CoverImage { get; set; }
/// <summary>
/// Total number of pages in all MangaFiles
/// </summary>
public int Pages { get; set; }
// Relationships
public Volume Volume { get; set; }
public int VolumeId { get; set; }
}
}

View File

@ -1,10 +1,18 @@

using System;
namespace API.Entities
{
public class FolderPath
{
public int Id { get; set; }
public string Path { get; set; }
/// <summary>
/// Used when scanning to see if we can skip if nothing has changed.
/// </summary>
public DateTime LastScanned { get; set; }
// Relationship
public Library Library { get; set; }
public int LibraryId { get; set; }
}

View File

@ -9,18 +9,14 @@ namespace API.Entities
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// Used to track if multiple MangaFiles (archives) represent a single Volume. If only one volume file, this will be 0.
/// </summary>
public int Chapter { get; set; }
/// <summary>
/// Number of pages for the given file
/// </summary>
public int NumberOfPages { get; set; }
public MangaFormat Format { get; set; }
// Relationship Mapping
public Volume Volume { get; set; }
public int VolumeId { get; set; }
public Chapter Chapter { get; set; }
public int ChapterId { get; set; }
}
}

View File

@ -12,14 +12,14 @@ namespace API.Entities
/// </summary>
public string Name { get; set; }
/// <summary>
/// Original Japanese Name
/// </summary>
public string OriginalName { get; set; }
/// <summary>
/// The name used to sort the Series. By default, will be the same as Name.
/// </summary>
public string SortName { get; set; }
/// <summary>
/// Original Name on disk. Not exposed to UI.
/// </summary>
public string OriginalName { get; set; }
/// <summary>
/// Summary information related to the Series
/// </summary>
public string Summary { get; set; }
@ -30,7 +30,7 @@ namespace API.Entities
/// Sum of all Volume page counts
/// </summary>
public int Pages { get; set; }
// Relationships
public ICollection<Volume> Volumes { get; set; }
public Library Library { get; set; }

View File

@ -9,11 +9,16 @@ namespace API.Entities
public int Id { get; set; }
public string Name { get; set; }
public int Number { get; set; }
public ICollection<MangaFile> Files { get; set; }
public ICollection<Chapter> Chapters { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public byte[] CoverImage { get; set; }
public int Pages { get; set; }
/// <summary>
/// Represents a Side story that is linked to the original Series. Omake, One Shot, etc.
/// </summary>
public bool IsSpecial { get; set; } = false;

View File

@ -51,7 +51,8 @@ namespace API.Extensions
if (file.Directory == null) continue;
var newName = $"{file.Directory.Name}_{file.Name}";
var newPath = Path.Join(root.FullName, newName);
file.MoveTo(newPath);
if (!File.Exists(newPath)) file.MoveTo(newPath);
}
}

View File

@ -16,6 +16,8 @@ namespace API.Helpers
CreateMap<Volume, VolumeDto>();
CreateMap<MangaFile, MangaFileDto>();
CreateMap<Chapter, ChapterDto>();
CreateMap<Series, SeriesDto>();

View File

@ -6,12 +6,12 @@ namespace API.Interfaces
public interface ICacheService
{
/// <summary>
/// Ensures the cache is created for the given volume and if not, will create it. Should be called before any other
/// Ensures the cache is created for the given chapter and if not, will create it. Should be called before any other
/// cache operations (except cleanup).
/// </summary>
/// <param name="volumeId"></param>
/// <returns>Volume for the passed volumeId. Side-effect from ensuring cache.</returns>
Task<Volume> Ensure(int volumeId);
/// <param name="chapterId"></param>
/// <returns>Chapter for the passed chapterId. Side-effect from ensuring cache.</returns>
Task<Chapter> Ensure(int chapterId);
/// <summary>
/// Clears cache directory of all folders and files.
@ -28,11 +28,11 @@ namespace API.Interfaces
/// <summary>
/// Returns the absolute path of a cached page.
/// </summary>
/// <param name="volume"></param>
/// <param name="chapter">Chapter entity with Files populated.</param>
/// <param name="page">Page number to look for</param>
/// <returns></returns>
(string path, MangaFile file) GetCachedPagePath(Volume volume, int page);
Task<(string path, MangaFile file)> GetCachedPagePath(Chapter chapter, int page);
bool CacheDirectoryIsAccessible();
void EnsureCacheDirectory();
}
}

View File

@ -7,6 +7,7 @@ namespace API.Interfaces
ISeriesRepository SeriesRepository { get; }
IUserRepository UserRepository { get; }
ILibraryRepository LibraryRepository { get; }
IVolumeRepository VolumeRepository { get; }
Task<bool> Complete();
bool HasChanges();
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
namespace API.Interfaces
{
public interface IVolumeRepository
{
void Update(Volume volume);
Task<Chapter> GetChapterAsync(int chapterId);
Task<ChapterDto> GetChapterDtoAsync(int chapterId);
Task<IList<MangaFile>> GetFilesForChapter(int chapterId);
}
}

View File

@ -161,7 +161,7 @@ namespace API.Services
var needsFlattening = ArchiveNeedsFlattening(archive);
if (!archive.HasFiles() && !needsFlattening) return;
archive.ExtractToDirectory(extractPath);
archive.ExtractToDirectory(extractPath, true);
_logger.LogDebug($"Extracted archive to {extractPath} in {sw.ElapsedMilliseconds} milliseconds.");
if (needsFlattening)

View File

@ -18,7 +18,7 @@ namespace API.Services
private readonly IArchiveService _archiveService;
private readonly IDirectoryService _directoryService;
private readonly NumericComparer _numericComparer;
public static readonly string CacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../cache/"));
public static readonly string CacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "cache/"));
public CacheService(ILogger<CacheService> logger, IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService)
{
@ -29,40 +29,36 @@ namespace API.Services
_numericComparer = new NumericComparer();
}
public bool CacheDirectoryIsAccessible()
public void EnsureCacheDirectory()
{
_logger.LogDebug($"Checking if valid Cache directory: {CacheDirectory}");
var di = new DirectoryInfo(CacheDirectory);
return di.Exists;
if (!di.Exists)
{
_logger.LogError($"Cache directory {CacheDirectory} is not accessible or does not exist. Creating...");
Directory.CreateDirectory(CacheDirectory);
}
}
public async Task<Volume> Ensure(int volumeId)
public async Task<Chapter> Ensure(int chapterId)
{
if (!CacheDirectoryIsAccessible())
{
return null;
}
Volume volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
EnsureCacheDirectory();
Chapter chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
foreach (var file in volume.Files)
foreach (var file in chapter.Files)
{
var extractPath = GetVolumeCachePath(volumeId, file);
var extractPath = GetCachePath(chapterId, file);
_archiveService.ExtractArchive(file.FilePath, extractPath);
}
return volume;
return chapter;
}
public void Cleanup()
{
_logger.LogInformation("Performing cleanup of Cache directory");
if (!CacheDirectoryIsAccessible())
{
_logger.LogError($"Cache directory {CacheDirectory} is not accessible or does not exist.");
return;
}
EnsureCacheDirectory();
DirectoryInfo di = new DirectoryInfo(CacheDirectory);
try
@ -79,6 +75,7 @@ namespace API.Services
public void CleanupVolumes(int[] volumeIds)
{
// TODO: Fix this code to work with chapters
_logger.LogInformation($"Running Cache cleanup on Volumes");
foreach (var volume in volumeIds)
@ -96,13 +93,19 @@ namespace API.Services
public string GetVolumeCachePath(int volumeId, MangaFile file)
/// <summary>
/// Returns the cache path for a given Chapter. Should be cacheDirectory/{chapterId}/
/// </summary>
/// <param name="chapterId"></param>
/// <param name="file"></param>
/// <returns></returns>
public string GetCachePath(int chapterId, MangaFile file)
{
var extractPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/"));
if (file.Chapter > 0)
{
extractPath = Path.Join(extractPath, file.Chapter + "");
}
var extractPath = Path.GetFullPath(Path.Join(CacheDirectory, $"{chapterId}/"));
// if (file.Chapter != null)
// {
// extractPath = Path.Join(extractPath, chapterId + "");
// }
return extractPath;
}
@ -110,28 +113,29 @@ namespace API.Services
{
// BUG: This causes a problem because total pages on a volume assumes "specials" to be there
//return files.OrderBy(f => f.Chapter).Where(f => f.Chapter > 0 || f.Volume.Number != 0);
return files.OrderBy(f => f.Chapter, new ChapterSortComparer());
return files;
//return files.OrderBy(f => f.Chapter, new ChapterSortComparer());
}
public (string path, MangaFile file) GetCachedPagePath(Volume volume, int page)
public async Task<(string path, MangaFile file)> GetCachedPagePath(Chapter chapter, int page)
{
// Calculate what chapter the page belongs to
var pagesSoFar = 0;
// Do not allow chapters with 0, as those are specials and break ordering for reading.
var orderedChapters = GetOrderedChapters(volume.Files);
foreach (var mangaFile in orderedChapters)
var chapterFiles = chapter.Files ?? await _unitOfWork.VolumeRepository.GetFilesForChapter(chapter.Id);
foreach (var mangaFile in chapterFiles)
{
if (page + 1 < (mangaFile.NumberOfPages + pagesSoFar))
{
var path = GetVolumeCachePath(volume.Id, mangaFile);
var path = GetCachePath(chapter.Id, mangaFile);
var files = _directoryService.GetFiles(path);
Array.Sort(files, _numericComparer);
return (files.ElementAt(page - pagesSoFar), mangaFile);
}
pagesSoFar += mangaFile.NumberOfPages;
}
return ("", null);
}
}

View File

@ -8,12 +8,19 @@ using System.Threading;
using System.Threading.Tasks;
using API.DTOs;
using API.Interfaces;
using Microsoft.Extensions.Logging;
using NetVips;
namespace API.Services
{
public class DirectoryService : IDirectoryService
{
private readonly ILogger<DirectoryService> _logger;
public DirectoryService(ILogger<DirectoryService> logger)
{
_logger = logger;
}
/// <summary>
/// Given a set of regex search criteria, get files in the given path.
@ -52,6 +59,11 @@ namespace API.Services
public async Task<ImageDto> ReadImageAsync(string imagePath)
{
if (!File.Exists(imagePath))
{
_logger.LogError("Image does not exist on disk.");
return null;
}
using var image = Image.NewFromFile(imagePath);
return new ImageDto

View File

@ -61,6 +61,12 @@ namespace API.Services
var totalFiles = 0;
foreach (var folderPath in library.Folders)
{
// if (!forceUpdate && Directory.GetLastWriteTime(folderPath.Path) <= folderPath.LastScanned)
// {
// _logger.LogDebug($"{folderPath.Path} hasn't been updated since last scan. Skipping.");
// continue;
// }
try {
totalFiles += DirectoryService.TraverseTreeParallelForEach(folderPath.Path, (f) =>
{
@ -88,10 +94,12 @@ namespace API.Services
// Remove series that are no longer on disk
RemoveSeriesNotOnDisk(allSeries, series, library);
foreach (var folder in library.Folders) folder.LastScanned = DateTime.Now;
_unitOfWork.LibraryRepository.Update(library);
if (Task.Run(() => _unitOfWork.Complete()).Result)
{
_logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series.");
}
else
@ -188,10 +196,10 @@ namespace API.Services
private Series UpdateSeries(Series series, ParserInfo[] infos, bool forceUpdate)
{
var volumes = UpdateVolumes(series, infos, forceUpdate);
var volumes = UpdateVolumesWithChapters(series, infos, forceUpdate);
series.Volumes = volumes;
series.Pages = volumes.Sum(v => v.Pages);
if (series.CoverImage == null || forceUpdate)
if (ShouldFindCoverImage(forceUpdate, series.CoverImage))
{
var firstCover = volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0);
if (firstCover == null && volumes.Any())
@ -213,16 +221,20 @@ namespace API.Services
{
_logger.LogDebug($"Creating File Entry for {info.FullFilePath}");
int.TryParse(info.Chapters, out var chapter);
_logger.LogDebug($"Found Chapter: {chapter}");
return new MangaFile()
{
FilePath = info.FullFilePath,
Chapter = chapter,
Format = info.Format,
NumberOfPages = info.Format == MangaFormat.Archive ? _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath): 1
};
}
private bool ShouldFindCoverImage(bool forceUpdate, byte[] coverImage)
{
return forceUpdate || coverImage == null || !coverImage.Any();
}
@ -237,16 +249,20 @@ namespace API.Services
{
ICollection<Volume> volumes = new List<Volume>();
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
//var justVolumes = infos.Select(pi => pi.Chapters == "0");
foreach (var info in infos)
{
var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
if (existingVolume != null)
{
var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
//var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
var existingFile = new MangaFile();
if (existingFile != null)
{
existingFile.Chapter = Parser.Parser.MinimumNumberFromRange(info.Chapters);
//existingFile.Chapter = Parser.Parser.MinimumNumberFromRange(info.Chapters);
existingFile.Format = info.Format;
existingFile.NumberOfPages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath);
}
@ -254,7 +270,7 @@ namespace API.Services
{
if (info.Format == MangaFormat.Archive)
{
existingVolume.Files.Add(CreateMangaFile(info));
// existingVolume.Files.Add(CreateMangaFile(info));
}
else
{
@ -271,7 +287,7 @@ namespace API.Services
existingVolume = volumes.SingleOrDefault(v => v.Name == info.Volumes);
if (existingVolume != null)
{
existingVolume.Files.Add(CreateMangaFile(info));
//existingVolume.Files.Add(CreateMangaFile(info));
}
else
{
@ -279,10 +295,10 @@ namespace API.Services
{
Name = info.Volumes,
Number = Parser.Parser.MinimumNumberFromRange(info.Volumes),
Files = new List<MangaFile>()
{
CreateMangaFile(info)
}
// Files = new List<MangaFile>()
// {
// CreateMangaFile(info)
// }
};
volumes.Add(vol);
}
@ -290,23 +306,121 @@ namespace API.Services
_logger.LogInformation($"Adding volume {volumes.Last().Number} with File: {info.Filename}");
}
foreach (var volume in volumes)
{
if (forceUpdate || volume.CoverImage == null || !volume.Files.Any())
{
var firstFile = volume.Files.OrderBy(x => x.Chapter).FirstOrDefault();
if (firstFile != null) volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
}
volume.Pages = volume.Files.Sum(x => x.NumberOfPages);
// if (forceUpdate || volume.CoverImage == null || !volume.Files.Any())
// {
// var firstFile = volume.Files.OrderBy(x => x.Chapter).FirstOrDefault();
// if (firstFile != null) volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
// }
//volume.Pages = volume.Files.Sum(x => x.NumberOfPages);
}
return volumes;
}
/// <summary>
///
/// </summary>
/// <param name="volume"></param>
/// <param name="infos"></param>
/// <param name="forceUpdate"></param>
/// <returns></returns>
private ICollection<Chapter> UpdateChapters(Volume volume, IEnumerable<ParserInfo> infos, bool forceUpdate)
{
var chapters = new List<Chapter>();
foreach (var info in infos)
{
volume.Chapters ??= new List<Chapter>();
var chapter = volume.Chapters.SingleOrDefault(c => c.Range == info.Chapters) ??
chapters.SingleOrDefault(v => v.Range == info.Chapters) ??
new Chapter()
{
Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + "",
Range = info.Chapters,
};
chapter.Files ??= new List<MangaFile>();
var existingFile = chapter?.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
if (existingFile != null)
{
existingFile.Format = info.Format;
existingFile.NumberOfPages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath);
}
else
{
if (info.Format == MangaFormat.Archive)
{
chapter.Files.Add(CreateMangaFile(info));
}
else
{
_logger.LogDebug($"Ignoring {info.Filename} as it is not an archive.");
}
}
chapter.Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + "";
chapter.Range = info.Chapters;
chapters.Add(chapter);
}
foreach (var chapter in chapters)
{
chapter.Pages = chapter.Files.Sum(f => f.NumberOfPages);
if (ShouldFindCoverImage(forceUpdate, chapter.CoverImage))
{
var firstFile = chapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
if (firstFile != null) chapter.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
}
}
return chapters;
}
private ICollection<Volume> UpdateVolumesWithChapters(Series series, ParserInfo[] infos, bool forceUpdate)
{
ICollection<Volume> volumes = new List<Volume>();
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
foreach (var info in infos)
{
var volume = (existingVolumes.SingleOrDefault(v => v.Name == info.Volumes) ??
volumes.SingleOrDefault(v => v.Name == info.Volumes)) ?? new Volume
{
Name = info.Volumes,
Number = Parser.Parser.MinimumNumberFromRange(info.Volumes),
};
var chapters = UpdateChapters(volume, infos.Where(pi => pi.Volumes == volume.Name).ToArray(), forceUpdate);
volume.Chapters = chapters;
volume.Pages = chapters.Sum(c => c.Pages);
volumes.Add(volume);
}
foreach (var volume in volumes)
{
if (ShouldFindCoverImage(forceUpdate, volume.CoverImage))
{
// TODO: Create a custom sorter for Chapters so it's consistent across the application
var firstChapter = volume.Chapters.OrderBy(x => Double.Parse(x.Number)).FirstOrDefault();
var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
if (firstFile != null) volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
}
}
return volumes;
}
public void ScanSeries(int libraryId, int seriesId)
{

View File

@ -3,6 +3,7 @@ using API.Middleware;
using Hangfire;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -57,7 +58,11 @@ namespace API
app.UseAuthorization();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default
});
app.UseEndpoints(endpoints =>
{