Merge pull request #46 from Kareadita/feature/compress-overhaul
server settings
1
.gitignore
vendored
@ -450,3 +450,4 @@ appsettings.json
|
||||
/API/Hangfire-log.db
|
||||
cache/
|
||||
/API/wwwroot/
|
||||
/API/cache/
|
@ -25,7 +25,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Services\Test Data" />
|
||||
<Folder Include="Services\Test Data\ArchiveService" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
19
API.Tests/ChapterSortComparerTest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Linq;
|
||||
using API.Comparators;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests
|
||||
{
|
||||
public class ChapterSortComparerTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(new[] {1, 2, 0}, new[] {1, 2, 0})]
|
||||
[InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})]
|
||||
[InlineData(new[] {1, 0, 0}, new[] {1, 0, 0})]
|
||||
public void ChapterSortTest(int[] input, int[] expected)
|
||||
{
|
||||
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparer()).ToArray());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
25
API.Tests/Converters/CronConverterTests.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using API.Helpers.Converters;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace API.Tests.Converters
|
||||
{
|
||||
public class CronConverterTests
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
|
||||
public CronConverterTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("daily", "0 0 * * *")]
|
||||
[InlineData("disabled", "0 0 31 2 *")]
|
||||
[InlineData("weekly", "0 0 * * 1")]
|
||||
public void ConvertTest(string input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
|
||||
}
|
||||
}
|
||||
}
|
@ -33,6 +33,9 @@ namespace API.Tests
|
||||
[InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
|
||||
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
|
||||
[InlineData("Dorohedoro v12 (2013) (Digital) (LostNerevarine-Empire).cbz", "12")]
|
||||
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")]
|
||||
[InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "0")]
|
||||
|
||||
public void ParseVolumeTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ParseVolume(filename));
|
||||
@ -74,6 +77,8 @@ namespace API.Tests
|
||||
[InlineData("Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip", "Ichinensei ni Nacchattara")]
|
||||
[InlineData("Chrno_Crusade_Dragon_Age_All_Stars[AS].zip", "")]
|
||||
[InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip", "Ichiban Ushiro no Daimaou")]
|
||||
[InlineData("Rent a Girlfriend v01.cbr", "Rent a Girlfriend")]
|
||||
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "Yumekui Merry")]
|
||||
//[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "Epigraph of the Closed Curve")]
|
||||
public void ParseSeriesTest(string filename, string expected)
|
||||
{
|
||||
@ -102,6 +107,9 @@ namespace API.Tests
|
||||
[InlineData("Mujaki no Rakuen Vol12 ch76", "76")]
|
||||
[InlineData("Beelzebub_01_[Noodles].zip", "1")]
|
||||
[InlineData("Yumekui-Merry_DKThias_Chapter21.zip", "21")]
|
||||
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")]
|
||||
[InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "11")]
|
||||
[InlineData("Beelzebub_53[KSH].zip", "53")]
|
||||
//[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "6")]
|
||||
public void ParseChaptersTest(string filename, string expected)
|
||||
{
|
||||
@ -158,6 +166,16 @@ namespace API.Tests
|
||||
{
|
||||
Assert.Equal(expected, ParseEdition(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("12-14", 12)]
|
||||
[InlineData("24", 24)]
|
||||
[InlineData("18-04", 4)]
|
||||
public void MinimumNumberFromRangeTest(string input, int expected)
|
||||
{
|
||||
Assert.Equal(expected, MinimumNumberFromRange(input));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void ParseInfoTest()
|
||||
|
73
API.Tests/Services/ArchiveServiceTests.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using API.Interfaces;
|
||||
using API.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Services
|
||||
{
|
||||
public class ArchiveServiceTests
|
||||
{
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly ILogger<ArchiveService> _logger = Substitute.For<ILogger<ArchiveService>>();
|
||||
|
||||
public ArchiveServiceTests()
|
||||
{
|
||||
_archiveService = new ArchiveService(_logger);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("flat file.zip", false)]
|
||||
[InlineData("file in folder in folder.zip", true)]
|
||||
[InlineData("file in folder.zip", true)]
|
||||
[InlineData("file in folder_alt.zip", true)]
|
||||
public void ArchiveNeedsFlatteningTest(string archivePath, bool expected)
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||
var file = Path.Join(testDirectory, archivePath);
|
||||
using ZipArchive archive = ZipFile.OpenRead(file);
|
||||
Assert.Equal(expected, _archiveService.ArchiveNeedsFlattening(archive));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("non existent file.zip", false)]
|
||||
[InlineData("wrong extension.rar", false)]
|
||||
[InlineData("empty.zip", false)]
|
||||
[InlineData("flat file.zip", true)]
|
||||
[InlineData("file in folder in folder.zip", true)]
|
||||
[InlineData("file in folder.zip", true)]
|
||||
[InlineData("file in folder_alt.zip", true)]
|
||||
public void IsValidArchiveTest(string archivePath, bool expected)
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||
Assert.Equal(expected, _archiveService.IsValidArchive(Path.Join(testDirectory, archivePath)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("non existent file.zip", 0)]
|
||||
[InlineData("wrong extension.rar", 0)]
|
||||
[InlineData("empty.zip", 0)]
|
||||
[InlineData("flat file.zip", 1)]
|
||||
[InlineData("file in folder in folder.zip", 1)]
|
||||
[InlineData("file in folder.zip", 1)]
|
||||
[InlineData("file in folder_alt.zip", 1)]
|
||||
public void GetNumberOfPagesFromArchiveTest(string archivePath, int expected)
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||
Assert.Equal(expected, _archiveService.GetNumberOfPagesFromArchive(Path.Join(testDirectory, archivePath)));
|
||||
}
|
||||
|
||||
[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/ArchiveService/CoverImages");
|
||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||
Assert.Equal(expectedBytes, _archiveService.GetCoverImage(Path.Join(testDirectory, inputFile)));
|
||||
}
|
||||
}
|
||||
}
|
98
API.Tests/Services/CacheServiceTests.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using API.Interfaces;
|
||||
using API.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Services
|
||||
{
|
||||
public class CacheServiceTests
|
||||
{
|
||||
// private readonly CacheService _cacheService;
|
||||
// private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
||||
// private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
// private readonly IArchiveService _archiveService = Substitute.For<IArchiveService>();
|
||||
// private readonly IDirectoryService _directoryService = Substitute.For<DirectoryService>();
|
||||
|
||||
public CacheServiceTests()
|
||||
{
|
||||
//_cacheService = new CacheService(_logger, _unitOfWork, _archiveService, _directoryService);
|
||||
}
|
||||
|
||||
//string GetCachedPagePath(Volume volume, int page)
|
||||
[Fact]
|
||||
//[InlineData("", 0, "")]
|
||||
public void GetCachedPagePathTest_Should()
|
||||
{
|
||||
// TODO: Figure out how to test this
|
||||
// string archivePath = "flat file.zip";
|
||||
// int pageNum = 0;
|
||||
// string expected = "cache/1/pexels-photo-6551949.jpg";
|
||||
//
|
||||
// var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||
// var file = Path.Join(testDirectory, archivePath);
|
||||
// var volume = new Volume
|
||||
// {
|
||||
// Id = 1,
|
||||
// Files = new List<MangaFile>()
|
||||
// {
|
||||
// new()
|
||||
// {
|
||||
// Id = 1,
|
||||
// Chapter = 0,
|
||||
// FilePath = archivePath,
|
||||
// Format = MangaFormat.Archive,
|
||||
// NumberOfPages = 1,
|
||||
// }
|
||||
// },
|
||||
// Name = "1",
|
||||
// Number = 1
|
||||
// };
|
||||
//
|
||||
// var cacheService = Substitute.ForPartsOf<CacheService>();
|
||||
// cacheService.Configure().CacheDirectoryIsAccessible().Returns(true);
|
||||
// cacheService.Configure().GetVolumeCachePath(1, volume.Files.ElementAt(0)).Returns("cache/1/");
|
||||
// _directoryService.Configure().GetFiles("cache/1/").Returns(new string[] {"pexels-photo-6551949.jpg"});
|
||||
// Assert.Equal(expected, _cacheService.GetCachedPagePath(volume, pageNum));
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrderedChaptersTest()
|
||||
{
|
||||
// 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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
|
||||
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));
|
||||
// // TODO: Implement this with ScannerService
|
||||
// //Assert.Equal(expectedBytes, ImageProvider.GetCoverImage(Path.Join(testDirectory, inputFile)));
|
||||
// }
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using API.Interfaces;
|
||||
using API.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
|
||||
namespace API.Tests.Services
|
||||
{
|
||||
public class ScannerServiceTests
|
||||
{
|
||||
private readonly ScannerService _scannerService;
|
||||
private readonly ILogger<ScannerService> _logger = Substitute.For<ILogger<ScannerService>>();
|
||||
private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
public ScannerServiceTests()
|
||||
{
|
||||
_scannerService = new ScannerService(_unitOfWork, _logger);
|
||||
}
|
||||
|
||||
// TODO: Start adding tests for how scanner works so we can ensure fallbacks, etc work
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ namespace API.Tests.Services
|
||||
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
|
||||
)]
|
||||
|
||||
public void TestLogicalComparer(string[] input, string[] expected)
|
||||
{
|
||||
NumericComparer nc = new NumericComparer();
|
||||
|
@ -0,0 +1,2 @@
|
||||
Files in this test are all royalty free and can be found here:
|
||||
https://www.pexels.com/photo/snow-wood-light-art-6551949/
|
BIN
API.Tests/Services/Test Data/ArchiveService/Archives/empty.zip
Normal file
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 395 KiB After Width: | Height: | Size: 395 KiB |
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 385 KiB |
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 344 KiB |
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 385 KiB |
18
API/Comparators/ChapterSortComparer.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.Comparators
|
||||
{
|
||||
public class ChapterSortComparer : IComparer<int>
|
||||
{
|
||||
public int Compare(int x, int y)
|
||||
{
|
||||
if (x == 0 && y == 0) return 0;
|
||||
// if x is 0, it comes second
|
||||
if (x == 0) return 1;
|
||||
// if y is 0, it comes second
|
||||
if (y == 0) return -1;
|
||||
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
}
|
||||
}
|
@ -20,16 +20,16 @@ namespace API.Comparators
|
||||
if (string.IsNullOrEmpty(s2)) return -1;
|
||||
|
||||
//WE style, special case
|
||||
bool sp1 = Char.IsLetterOrDigit(s1, 0);
|
||||
bool sp2 = Char.IsLetterOrDigit(s2, 0);
|
||||
var sp1 = Char.IsLetterOrDigit(s1, 0);
|
||||
var sp2 = Char.IsLetterOrDigit(s2, 0);
|
||||
if(sp1 && !sp2) return 1;
|
||||
if(!sp1 && sp2) return -1;
|
||||
|
||||
int i1 = 0, i2 = 0; //current index
|
||||
while(true)
|
||||
{
|
||||
bool c1 = Char.IsDigit(s1, i1);
|
||||
bool c2 = Char.IsDigit(s2, i2);
|
||||
var c1 = Char.IsDigit(s1, i1);
|
||||
var c2 = Char.IsDigit(s2, i2);
|
||||
int r; // temp result
|
||||
if(!c1 && !c2)
|
||||
{
|
||||
|
@ -20,9 +20,5 @@ namespace API.Controllers
|
||||
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
||||
return users.Count > 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System.IO;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
|
@ -145,7 +145,7 @@ namespace API.Controllers
|
||||
[HttpPost("scan")]
|
||||
public ActionResult Scan(int libraryId)
|
||||
{
|
||||
_taskScheduler.ScanLibrary(libraryId, false);
|
||||
_taskScheduler.ScanLibrary(libraryId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -177,13 +177,13 @@ namespace API.Controllers
|
||||
var username = User.GetUsername();
|
||||
_logger.LogInformation($"Library {libraryId} is being deleted by {username}.");
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
|
||||
var volumes = (await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(series.Select(x => x.Id).ToArray()))
|
||||
.Select(x => x.Id).ToArray();
|
||||
var chapterIds =
|
||||
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(series.Select(x => x.Id).ToArray());
|
||||
var result = await _unitOfWork.LibraryRepository.DeleteLibrary(libraryId);
|
||||
|
||||
if (result && volumes.Any())
|
||||
if (result && chapterIds.Any())
|
||||
{
|
||||
_taskScheduler.CleanupVolumes(volumes);
|
||||
_taskScheduler.CleanupChapters(chapterIds);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
|
@ -28,24 +28,28 @@ 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 = _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.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);
|
||||
|
||||
@ -56,10 +60,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)
|
||||
{
|
||||
@ -69,13 +75,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);
|
||||
@ -84,8 +91,7 @@ namespace API.Controllers
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
@ -36,17 +35,22 @@ namespace API.Controllers
|
||||
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
|
||||
{
|
||||
var username = User.GetUsername();
|
||||
var volumes = (await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(new []{seriesId})).Select(x => x.Id).ToArray();
|
||||
var chapterIds = (await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{seriesId}));
|
||||
_logger.LogInformation($"Series {seriesId} is being deleted by {username}.");
|
||||
var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId);
|
||||
|
||||
if (result)
|
||||
{
|
||||
_taskScheduler.CleanupVolumes(volumes);
|
||||
_taskScheduler.CleanupChapters(chapterIds);
|
||||
}
|
||||
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,6 +65,12 @@ namespace API.Controllers
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, user.Id));
|
||||
}
|
||||
|
||||
[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)
|
||||
|
@ -1,17 +1,13 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Converters;
|
||||
using API.Interfaces;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers
|
||||
@ -19,32 +15,27 @@ namespace API.Controllers
|
||||
[Authorize]
|
||||
public class SettingsController : BaseApiController
|
||||
{
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly ILogger<SettingsController> _logger;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public SettingsController(DataContext dataContext, ILogger<SettingsController> logger, IMapper mapper, ITaskScheduler taskScheduler)
|
||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork)
|
||||
{
|
||||
_dataContext = dataContext;
|
||||
_logger = logger;
|
||||
_mapper = mapper;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||
{
|
||||
var settings = await _dataContext.ServerSetting.Select(x => x).ToListAsync();
|
||||
return _mapper.Map<ServerSettingDto>(settings);
|
||||
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("")]
|
||||
public async Task<ActionResult> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
_logger.LogInformation($"{User.GetUsername()} is updating Server Settings");
|
||||
|
||||
|
||||
if (updateSettingsDto.CacheDirectory.Equals(string.Empty))
|
||||
{
|
||||
return BadRequest("Cache Directory cannot be empty");
|
||||
@ -54,13 +45,39 @@ namespace API.Controllers
|
||||
{
|
||||
return BadRequest("Directory does not exist or is not accessible.");
|
||||
}
|
||||
// TODO: Figure out how to handle a change. This means that on clean, we need to clean up old cache
|
||||
// directory and new one, but what if someone is reading?
|
||||
// I can just clean both always, /cache/ is an owned folder, so users shouldn't use it.
|
||||
|
||||
|
||||
//_dataContext.ServerSetting.Update
|
||||
return BadRequest("Not Implemented");
|
||||
|
||||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
|
||||
foreach (var setting in currentSettings)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskBackup;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.TaskScan;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.Complete())
|
||||
{
|
||||
_logger.LogInformation("Server Settings updated.");
|
||||
return Ok(updateSettingsDto);
|
||||
}
|
||||
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("task-frequencies")]
|
||||
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
|
||||
{
|
||||
return Ok(CronConverter.Options);
|
||||
}
|
||||
}
|
||||
}
|
31
API/DTOs/ChapterDto.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -9,5 +9,7 @@
|
||||
public int Height { get; init; }
|
||||
public string Format { get; init; }
|
||||
public byte[] Content { get; init; }
|
||||
public int Chapter { get; set; }
|
||||
public string MangaFileName { get; set; }
|
||||
}
|
||||
}
|
12
API/DTOs/MangaFileDto.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using API.Entities;
|
||||
|
||||
namespace API.DTOs
|
||||
{
|
||||
public class MangaFileDto
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
public int NumberOfPages { get; set; }
|
||||
public MangaFormat Format { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -3,7 +3,8 @@
|
||||
public class ServerSettingDto
|
||||
{
|
||||
public string CacheDirectory { get; set; }
|
||||
// public string Kind { get; init; }
|
||||
// public string Value { get; init; }
|
||||
public string TaskScan { get; set; }
|
||||
public string LoggingLevel { get; set; }
|
||||
public string TaskBackup { get; set; }
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs
|
||||
{
|
||||
public class VolumeDto
|
||||
@ -9,5 +11,6 @@ namespace API.DTOs
|
||||
public byte[] CoverImage { get; set; }
|
||||
public int Pages { get; set; }
|
||||
public int PagesRead { get; set; }
|
||||
public ICollection<ChapterDto> Chapters { get; set; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
@ -30,10 +33,6 @@ namespace API.Data
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
// builder.Entity<ServerSetting>()
|
||||
// .HasAlternateKey(s => s.Key)
|
||||
// .HasName("AlternateKey_Key");
|
||||
|
||||
builder.Entity<AppUser>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.User)
|
||||
|
688
API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
111
API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
676
API/Data/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
676
API/Data/Migrations/20210203164258_ServerSettingsKey.Designer.cs
generated
Normal 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("20210203164258_ServerSettingsKey")]
|
||||
partial class ServerSettingsKey
|
||||
{
|
||||
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<int>("Key")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
29
API/Data/Migrations/20210203164258_ServerSettingsKey.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class ServerSettingsKey : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "Key",
|
||||
table: "ServerSetting",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Key",
|
||||
table: "ServerSetting",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
});
|
||||
@ -298,8 +335,8 @@ namespace API.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
b.Property<int>("Key")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
@ -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
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
@ -31,20 +30,24 @@ namespace API.Data
|
||||
|
||||
public static async Task SeedSettings(DataContext context)
|
||||
{
|
||||
// NOTE: This needs to check if settings already exists before inserting.
|
||||
// IList<ServerSetting> defaultSettings = new List<ServerSetting>()
|
||||
// {
|
||||
// new ServerSetting() {Key = "CacheDirectory", Value = CacheService.CacheDirectory}
|
||||
// };
|
||||
//
|
||||
// await context.ServerSetting.AddRangeAsync(defaultSettings);
|
||||
// await context.SaveChangesAsync();
|
||||
// await context.ServerSetting.AddAsync(new ServerSetting
|
||||
// {
|
||||
// CacheDirectory = CacheService.CacheDirectory
|
||||
// });
|
||||
//
|
||||
// await context.SaveChangesAsync();
|
||||
context.Database.EnsureCreated();
|
||||
|
||||
IList<ServerSetting> defaultSettings = new List<ServerSetting>()
|
||||
{
|
||||
new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory},
|
||||
new () {Key = ServerSettingKey.TaskScan, Value = "daily"}
|
||||
};
|
||||
|
||||
foreach (var defaultSetting in defaultSettings)
|
||||
{
|
||||
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
|
||||
if (existing == null)
|
||||
{
|
||||
context.ServerSetting.Add(defaultSetting);
|
||||
}
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -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,10 +129,11 @@ 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);
|
||||
|
||||
@ -169,6 +173,30 @@ namespace API.Data
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds)
|
||||
{
|
||||
var series = await _context.Series
|
||||
.Where(s => seriesIds.Contains(s.Id))
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ToListAsync();
|
||||
|
||||
// TODO: refactor this
|
||||
IList<int> chapterIds = new List<int>();
|
||||
foreach (var s in series)
|
||||
{
|
||||
foreach (var v in s.Volumes)
|
||||
{
|
||||
foreach (var c in v.Chapters)
|
||||
{
|
||||
chapterIds.Add(c.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chapterIds.ToArray();
|
||||
}
|
||||
|
||||
private async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
|
||||
{
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
@ -197,6 +225,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);
|
||||
}
|
||||
}
|
||||
|
47
API/Data/SettingsRepository.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Interfaces;
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
{
|
||||
public class SettingsRepository : ISettingsRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public SettingsRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Update(ServerSetting settings)
|
||||
{
|
||||
_context.Entry(settings).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public async Task<ServerSettingDto> GetSettingsDtoAsync()
|
||||
{
|
||||
var settings = await _context.ServerSetting
|
||||
.Select(x => x)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
return _mapper.Map<ServerSettingDto>(settings);
|
||||
}
|
||||
|
||||
public Task<ServerSetting> GetSettingAsync(ServerSettingKey key)
|
||||
{
|
||||
return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ServerSetting>> GetSettingsAsync()
|
||||
{
|
||||
return await _context.ServerSetting.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,10 @@ 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 ISettingsRepository SettingsRepository => new SettingsRepository(_context, _mapper);
|
||||
|
||||
public async Task<bool> Complete()
|
||||
{
|
||||
|
61
API/Data/VolumeRepository.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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
@ -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; }
|
||||
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
@ -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; }
|
||||
@ -27,10 +27,10 @@ namespace API.Entities
|
||||
public DateTime LastModified { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
/// <summary>
|
||||
/// Sum of all Volume pages
|
||||
/// Sum of all Volume page counts
|
||||
/// </summary>
|
||||
public int Pages { get; set; }
|
||||
|
||||
|
||||
// Relationships
|
||||
public ICollection<Volume> Volumes { get; set; }
|
||||
public Library Library { get; set; }
|
||||
|
@ -1,13 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Entities.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Entities
|
||||
{
|
||||
public class ServerSetting : IHasConcurrencyToken
|
||||
{
|
||||
[Key]
|
||||
public string Key { get; set; }
|
||||
public ServerSettingKey Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
|
||||
[ConcurrencyCheck]
|
||||
|
10
API/Entities/ServerSettingKey.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace API.Entities
|
||||
{
|
||||
public enum ServerSettingKey
|
||||
{
|
||||
TaskScan = 0,
|
||||
CacheDirectory = 1,
|
||||
TaskBackup = 2,
|
||||
LoggingLevel = 3
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
||||
|
||||
|
@ -23,6 +23,7 @@ namespace API.Extensions
|
||||
services.AddScoped<ICacheService, CacheService>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
services.AddScoped<IScannerService, ScannerService>();
|
||||
services.AddScoped<IArchiveService, ArchiveService>();
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
|
||||
namespace API.Extensions
|
||||
{
|
||||
@ -51,8 +49,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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,10 @@ namespace API.Helpers
|
||||
|
||||
CreateMap<Volume, VolumeDto>();
|
||||
|
||||
CreateMap<MangaFile, MangaFileDto>();
|
||||
|
||||
CreateMap<Chapter, ChapterDto>();
|
||||
|
||||
CreateMap<Series, SeriesDto>();
|
||||
|
||||
CreateMap<Library, LibraryDto>()
|
||||
|
41
API/Helpers/Converters/CronConverter.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using Hangfire;
|
||||
|
||||
namespace API.Helpers.Converters
|
||||
{
|
||||
public static class CronConverter
|
||||
{
|
||||
public static readonly IEnumerable<string> Options = new []
|
||||
{
|
||||
"disabled",
|
||||
"daily",
|
||||
"weekly",
|
||||
};
|
||||
public static string ConvertToCronNotation(string source)
|
||||
{
|
||||
string destination = "";
|
||||
destination = source.ToLower() switch
|
||||
{
|
||||
"daily" => Cron.Daily(),
|
||||
"weekly" => Cron.Weekly(),
|
||||
"disabled" => Cron.Never(),
|
||||
"" => Cron.Never(),
|
||||
_ => destination
|
||||
};
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
public static string ConvertFromCronNotation(string cronNotation)
|
||||
{
|
||||
string destination = "";
|
||||
destination = cronNotation.ToLower() switch
|
||||
{
|
||||
"0 0 31 2 *" => "disabled",
|
||||
_ => destination
|
||||
};
|
||||
|
||||
return destination;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,16 +9,24 @@ namespace API.Helpers.Converters
|
||||
{
|
||||
public ServerSettingDto Convert(IEnumerable<ServerSetting> source, ServerSettingDto destination, ResolutionContext context)
|
||||
{
|
||||
destination = new ServerSettingDto();
|
||||
destination ??= new ServerSettingDto();
|
||||
foreach (var row in source)
|
||||
{
|
||||
switch (row.Key)
|
||||
{
|
||||
case "CacheDirectory":
|
||||
case ServerSettingKey.CacheDirectory:
|
||||
destination.CacheDirectory = row.Value;
|
||||
break;
|
||||
default:
|
||||
case ServerSettingKey.TaskScan:
|
||||
destination.TaskScan = row.Value;
|
||||
break;
|
||||
case ServerSettingKey.LoggingLevel:
|
||||
destination.LoggingLevel = row.Value;
|
||||
break;
|
||||
case ServerSettingKey.TaskBackup:
|
||||
destination.TaskBackup = row.Value;
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
13
API/Interfaces/IArchiveService.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace API.Interfaces
|
||||
{
|
||||
public interface IArchiveService
|
||||
{
|
||||
bool ArchiveNeedsFlattening(ZipArchive archive);
|
||||
void ExtractArchive(string archivePath, string extractPath);
|
||||
int GetNumberOfPagesFromArchive(string archivePath);
|
||||
byte[] GetCoverImage(string filepath, bool createThumbnail = false);
|
||||
bool IsValidArchive(string archivePath);
|
||||
}
|
||||
}
|
@ -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.
|
||||
@ -21,16 +21,18 @@ namespace API.Interfaces
|
||||
/// <summary>
|
||||
/// Clears cache directory of all volumes. This can be invoked from deleting a library or a series.
|
||||
/// </summary>
|
||||
/// <param name="volumeIds">Volumes that belong to that library. Assume the library might have been deleted before this invocation.</param>
|
||||
void CleanupVolumes(int[] volumeIds);
|
||||
/// <param name="chapterIds">Volumes that belong to that library. Assume the library might have been deleted before this invocation.</param>
|
||||
void CleanupChapters(int[] chapterIds);
|
||||
|
||||
|
||||
/// <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 GetCachedPagePath(Volume volume, int page);
|
||||
Task<(string path, MangaFile file)> GetCachedPagePath(Chapter chapter, int page);
|
||||
|
||||
void EnsureCacheDirectory();
|
||||
}
|
||||
}
|
@ -14,5 +14,12 @@ namespace API.Interfaces
|
||||
IEnumerable<string> ListDirectory(string rootPath);
|
||||
|
||||
Task<ImageDto> ReadImageAsync(string imagePath);
|
||||
/// <summary>
|
||||
/// Gets files in a directory. If searchPatternExpression is passed, will match the regex against for filtering.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="searchPatternExpression"></param>
|
||||
/// <returns></returns>
|
||||
string[] GetFiles(string path, string searchPatternExpression = "");
|
||||
}
|
||||
}
|
@ -1,7 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
|
||||
namespace API.Interfaces
|
||||
namespace API.Interfaces
|
||||
{
|
||||
public interface IScannerService
|
||||
{
|
||||
|
@ -16,14 +16,12 @@ namespace API.Interfaces
|
||||
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId);
|
||||
IEnumerable<Volume> GetVolumes(int seriesId);
|
||||
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
||||
|
||||
Task<Volume> GetVolumeAsync(int volumeId);
|
||||
Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId);
|
||||
|
||||
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds);
|
||||
Task<bool> DeleteSeriesAsync(int seriesId);
|
||||
Task<Volume> GetVolumeByIdAsync(int volumeId);
|
||||
Task<Series> GetSeriesByIdAsync(int seriesId);
|
||||
|
||||
Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds);
|
||||
}
|
||||
}
|
16
API/Interfaces/ISettingsRepository.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
|
||||
namespace API.Interfaces
|
||||
{
|
||||
public interface ISettingsRepository
|
||||
{
|
||||
void Update(ServerSetting settings);
|
||||
Task<ServerSettingDto> GetSettingsDtoAsync();
|
||||
Task<ServerSetting> GetSettingAsync(ServerSettingKey key);
|
||||
Task<IEnumerable<ServerSetting>> GetSettingsAsync();
|
||||
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
public interface ITaskScheduler
|
||||
{
|
||||
void ScanLibrary(int libraryId, bool forceUpdate = false);
|
||||
void CleanupVolumes(int[] volumeIds);
|
||||
void CleanupChapters(int[] chapterIds);
|
||||
void ScanSeries(int libraryId, int seriesId);
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ namespace API.Interfaces
|
||||
ISeriesRepository SeriesRepository { get; }
|
||||
IUserRepository UserRepository { get; }
|
||||
ILibraryRepository LibraryRepository { get; }
|
||||
IVolumeRepository VolumeRepository { get; }
|
||||
ISettingsRepository SettingsRepository { get; }
|
||||
Task<bool> Complete();
|
||||
bool HasChanges();
|
||||
}
|
||||
|
15
API/Interfaces/IVolumeRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.Entities;
|
||||
|
||||
@ -9,6 +11,8 @@ namespace API.Parser
|
||||
{
|
||||
public static readonly string MangaFileExtensions = @"\.cbz|\.zip"; // |\.rar|\.cbr
|
||||
public static readonly string ImageFileExtensions = @"\.png|\.jpeg|\.jpg|\.gif";
|
||||
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex MangaFileRegex = new Regex(MangaFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
//?: is a non-capturing group in C#, else anything in () will be a group
|
||||
private static readonly Regex[] MangaVolumeRegex = new[]
|
||||
@ -125,7 +129,7 @@ namespace API.Parser
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
// Beelzebub_01_[Noodles].zip
|
||||
new Regex(
|
||||
@"^((?!v|vo|vol|Volume).)*( |_)(?<Chapter>\.?\d+)( |_)",
|
||||
@"^((?!v|vo|vol|Volume).)*( |_)(?<Chapter>\.?\d+)( |_|\[|\()",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
// Yumekui-Merry_DKThias_Chapter21.zip
|
||||
new Regex(
|
||||
@ -168,7 +172,7 @@ namespace API.Parser
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="rootPath">Root folder</param>
|
||||
/// <returns><see cref="ParserInfo"/> or null if Series was empty</returns>
|
||||
public static ParserInfo? Parse(string filePath, string rootPath)
|
||||
public static ParserInfo Parse(string filePath, string rootPath)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var directoryName = (new FileInfo(filePath)).Directory?.Name;
|
||||
@ -387,13 +391,19 @@ namespace API.Parser
|
||||
public static bool IsArchive(string filePath)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
return MangaFileExtensions.Contains(fileInfo.Extension);
|
||||
return MangaFileRegex.IsMatch(fileInfo.Extension);
|
||||
}
|
||||
|
||||
public static bool IsImage(string filePath)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
return ImageFileExtensions.Contains(fileInfo.Extension);
|
||||
return ImageRegex.IsMatch(fileInfo.Extension);
|
||||
}
|
||||
|
||||
public static int MinimumNumberFromRange(string range)
|
||||
{
|
||||
var tokens = range.Split("-");
|
||||
return tokens.Min(Int32.Parse);
|
||||
}
|
||||
}
|
||||
}
|
@ -44,11 +44,6 @@ namespace API
|
||||
|
||||
private static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
// .ConfigureLogging(logging =>
|
||||
// {
|
||||
// logging.ClearProviders();
|
||||
// logging.AddConsole();
|
||||
// })
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
|
175
API/Services/ArchiveService.cs
Normal file
@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
|
||||
namespace API.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Responsible for manipulating Archive files. Used by <see cref="CacheService"/> and <see cref="ScannerService"/>
|
||||
/// </summary>
|
||||
public class ArchiveService : IArchiveService
|
||||
{
|
||||
private readonly ILogger<ArchiveService> _logger;
|
||||
private const int ThumbnailWidth = 320;
|
||||
|
||||
public ArchiveService(ILogger<ArchiveService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public int GetNumberOfPagesFromArchive(string archivePath)
|
||||
{
|
||||
if (!IsValidArchive(archivePath)) return 0;
|
||||
_logger.LogDebug($"Getting Page numbers from {archivePath}");
|
||||
|
||||
try
|
||||
{
|
||||
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
||||
return archive.Entries.Count(e => Parser.Parser.IsImage(e.FullName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when reading archive stream.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 byte[] GetCoverImage(string filepath, bool createThumbnail = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsValidArchive(filepath)) return Array.Empty<byte>();
|
||||
_logger.LogDebug($"Extracting Cover image from {filepath}");
|
||||
|
||||
using ZipArchive archive = ZipFile.OpenRead(filepath);
|
||||
if (!archive.HasFiles()) return Array.Empty<byte>();
|
||||
|
||||
var folder = archive.Entries.SingleOrDefault(x => Path.GetFileNameWithoutExtension(x.Name).ToLower() == "folder");
|
||||
var entries = archive.Entries.Where(x => Path.HasExtension(x.FullName) && Parser.Parser.IsImage(x.FullName)).OrderBy(x => x.FullName).ToList();
|
||||
var entry = folder ?? entries[0];
|
||||
|
||||
return createThumbnail ? CreateThumbnail(entry) : ConvertEntryToByteArray(entry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when reading archive stream.");
|
||||
}
|
||||
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
private byte[] CreateThumbnail(ZipArchiveEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth);
|
||||
return thumbnail.WriteToBuffer(".jpg");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was a critical error and prevented thumbnail generation. Defaulting to no cover image.");
|
||||
}
|
||||
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
private static byte[] ConvertEntryToByteArray(ZipArchiveEntry entry)
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
var data = ms.ToArray();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given an archive stream, will assess whether directory needs to be flattened so that the extracted archive files are directly
|
||||
/// under extract path and not nested in subfolders. See <see cref="DirectoryInfoExtensions"/> Flatten method.
|
||||
/// </summary>
|
||||
/// <param name="archive">An opened archive stream</param>
|
||||
/// <returns></returns>
|
||||
public bool ArchiveNeedsFlattening(ZipArchive archive)
|
||||
{
|
||||
// Sometimes ZipArchive will list the directory and others it will just keep it in the FullName
|
||||
return archive.Entries.Count > 0 &&
|
||||
!Path.HasExtension(archive.Entries.ElementAt(0).FullName) ||
|
||||
archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if the archive path exists and there are images inside it. This will log as an error.
|
||||
/// </summary>
|
||||
/// <param name="archivePath"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsValidArchive(string archivePath)
|
||||
{
|
||||
if (!File.Exists(archivePath))
|
||||
{
|
||||
_logger.LogError($"Archive {archivePath} could not be found.");
|
||||
return false;
|
||||
}
|
||||
if (!Parser.Parser.IsArchive(archivePath))
|
||||
{
|
||||
_logger.LogError($"Archive {archivePath} is not a valid archive.");
|
||||
return false;
|
||||
}
|
||||
|
||||
using var archive = ZipFile.OpenRead(archivePath);
|
||||
if (archive.Entries.Any(e => Parser.Parser.IsImage(e.FullName))) return true;
|
||||
_logger.LogError($"Archive {archivePath} contains no images.");
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts an archive to a temp cache directory. Returns path to new directory. If temp cache directory already exists,
|
||||
/// will return that without performing an extraction. Returns empty string if there are any invalidations which would
|
||||
/// prevent operations to perform correctly (missing archivePath file, empty archive, etc).
|
||||
/// </summary>
|
||||
/// <param name="archivePath">A valid file to an archive file.</param>
|
||||
/// <param name="extractPath">Path to extract to</param>
|
||||
/// <returns></returns>
|
||||
public void ExtractArchive(string archivePath, string extractPath)
|
||||
{
|
||||
if (!IsValidArchive(archivePath)) return;
|
||||
|
||||
if (Directory.Exists(extractPath))
|
||||
{
|
||||
_logger.LogDebug($"Archive {archivePath} has already been extracted. Returning existing folder.");
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
||||
var needsFlattening = ArchiveNeedsFlattening(archive);
|
||||
if (!archive.HasFiles() && !needsFlattening) return;
|
||||
|
||||
archive.ExtractToDirectory(extractPath, true);
|
||||
_logger.LogDebug($"Extracted archive to {extractPath} in {sw.ElapsedMilliseconds} milliseconds.");
|
||||
|
||||
if (needsFlattening)
|
||||
{
|
||||
sw = Stopwatch.StartNew();
|
||||
_logger.LogInformation("Extracted archive is nested in root folder, flattening...");
|
||||
new DirectoryInfo(extractPath).Flatten();
|
||||
_logger.LogInformation($"Flattened in {sw.ElapsedMilliseconds} milliseconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
@ -13,54 +12,52 @@ namespace API.Services
|
||||
{
|
||||
public class CacheService : ICacheService
|
||||
{
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ILogger<CacheService> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
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(IDirectoryService directoryService, ILogger<CacheService> logger, IUnitOfWork unitOfWork)
|
||||
public CacheService(ILogger<CacheService> logger, IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService)
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_archiveService = archiveService;
|
||||
_directoryService = directoryService;
|
||||
_numericComparer = new NumericComparer();
|
||||
}
|
||||
|
||||
private 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())
|
||||
EnsureCacheDirectory();
|
||||
Chapter chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||
|
||||
foreach (var file in chapter.Files)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Volume volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
||||
foreach (var file in volume.Files)
|
||||
{
|
||||
var extractPath = GetVolumeCachePath(volumeId, file);
|
||||
|
||||
ExtractArchive(file.FilePath, extractPath);
|
||||
var extractPath = GetCachePath(chapterId);
|
||||
_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
|
||||
@ -75,13 +72,13 @@ namespace API.Services
|
||||
_logger.LogInformation("Cache directory purged.");
|
||||
}
|
||||
|
||||
public void CleanupVolumes(int[] volumeIds)
|
||||
public void CleanupChapters(int[] chapterIds)
|
||||
{
|
||||
_logger.LogInformation($"Running Cache cleanup on Volumes");
|
||||
|
||||
foreach (var volume in volumeIds)
|
||||
foreach (var chapter in chapterIds)
|
||||
{
|
||||
var di = new DirectoryInfo(Path.Join(CacheDirectory, volume + ""));
|
||||
var di = new DirectoryInfo(GetCachePath(chapter));
|
||||
if (di.Exists)
|
||||
{
|
||||
di.Delete(true);
|
||||
@ -90,75 +87,38 @@ namespace API.Services
|
||||
}
|
||||
_logger.LogInformation("Cache directory purged");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Extracts an archive to a temp cache directory. Returns path to new directory. If temp cache directory already exists,
|
||||
/// will return that without performing an extraction. Returns empty string if there are any invalidations which would
|
||||
/// prevent operations to perform correctly (missing archivePath file, empty archive, etc).
|
||||
/// Returns the cache path for a given Chapter. Should be cacheDirectory/{chapterId}/
|
||||
/// </summary>
|
||||
/// <param name="archivePath">A valid file to an archive file.</param>
|
||||
/// <param name="extractPath">Path to extract to</param>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
private string ExtractArchive(string archivePath, string extractPath)
|
||||
private string GetCachePath(int chapterId)
|
||||
{
|
||||
// NOTE: This is used by Cache Service
|
||||
if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath))
|
||||
{
|
||||
_logger.LogError($"Archive {archivePath} could not be found.");
|
||||
return "";
|
||||
}
|
||||
|
||||
if (Directory.Exists(extractPath))
|
||||
{
|
||||
_logger.LogDebug($"Archive {archivePath} has already been extracted. Returning existing folder.");
|
||||
return extractPath;
|
||||
}
|
||||
|
||||
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
||||
// TODO: Throw error if we couldn't extract
|
||||
var needsFlattening = archive.Entries.Count > 0 && !Path.HasExtension(archive.Entries.ElementAt(0).FullName);
|
||||
if (!archive.HasFiles() && !needsFlattening) return "";
|
||||
|
||||
archive.ExtractToDirectory(extractPath);
|
||||
_logger.LogDebug($"Extracting archive to {extractPath}");
|
||||
|
||||
if (!needsFlattening) return extractPath;
|
||||
|
||||
_logger.LogInformation("Extracted archive is nested in root folder, flattening...");
|
||||
new DirectoryInfo(extractPath).Flatten();
|
||||
|
||||
return extractPath;
|
||||
return Path.GetFullPath(Path.Join(CacheDirectory, $"{chapterId}/"));
|
||||
}
|
||||
|
||||
|
||||
private string GetVolumeCachePath(int volumeId, MangaFile file)
|
||||
{
|
||||
var extractPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/"));
|
||||
if (file.Chapter > 0)
|
||||
{
|
||||
extractPath = Path.Join(extractPath, file.Chapter + "");
|
||||
}
|
||||
return extractPath;
|
||||
}
|
||||
|
||||
public string 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;
|
||||
foreach (var mangaFile in volume.Files.OrderBy(f => f.Chapter))
|
||||
var chapterFiles = chapter.Files ?? await _unitOfWork.VolumeRepository.GetFilesForChapter(chapter.Id);
|
||||
foreach (var mangaFile in chapterFiles)
|
||||
{
|
||||
if (page + 1 < (mangaFile.NumberOfPages + pagesSoFar))
|
||||
if (page < (mangaFile.NumberOfPages + pagesSoFar))
|
||||
{
|
||||
var path = GetVolumeCachePath(volume.Id, mangaFile);
|
||||
var files = DirectoryService.GetFiles(path);
|
||||
var path = GetCachePath(chapter.Id);
|
||||
var files = _directoryService.GetFiles(path, Parser.Parser.ImageFileExtensions);
|
||||
Array.Sort(files, _numericComparer);
|
||||
|
||||
return files.ElementAt(page - pagesSoFar);
|
||||
return (files.ElementAt(page - pagesSoFar), mangaFile);
|
||||
}
|
||||
|
||||
|
||||
pagesSoFar += mangaFile.NumberOfPages;
|
||||
}
|
||||
return "";
|
||||
|
||||
return ("", null);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
@ -33,10 +40,14 @@ namespace API.Services
|
||||
reSearchPattern.IsMatch(Path.GetExtension(file)));
|
||||
}
|
||||
|
||||
public static string[] GetFiles(string path)
|
||||
public string[] GetFiles(string path, string searchPatternExpression = "")
|
||||
{
|
||||
if (!Directory.Exists(path)) return Array.Empty<string>();
|
||||
return Directory.GetFiles(path);
|
||||
if (searchPatternExpression != string.Empty)
|
||||
{
|
||||
return GetFilesWithCertainExtensions(path, searchPatternExpression).ToArray();
|
||||
}
|
||||
|
||||
return !Directory.Exists(path) ? Array.Empty<string>() : Directory.GetFiles(path);
|
||||
}
|
||||
|
||||
public IEnumerable<string> ListDirectory(string rootPath)
|
||||
@ -48,12 +59,16 @@ namespace API.Services
|
||||
.Where(dir => !(dir.Attributes.HasFlag(FileAttributes.Hidden) || dir.Attributes.HasFlag(FileAttributes.System)))
|
||||
.Select(d => d.Name).ToImmutableList();
|
||||
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
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
|
||||
@ -63,7 +78,7 @@ namespace API.Services
|
||||
FullPath = Path.GetFullPath(imagePath),
|
||||
Width = image.Width,
|
||||
Height = image.Height,
|
||||
Format = image.Format
|
||||
Format = image.Format,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -4,16 +4,12 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using API.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
|
||||
namespace API.Services
|
||||
{
|
||||
@ -21,12 +17,14 @@ namespace API.Services
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ScannerService> _logger;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private ConcurrentDictionary<string, List<ParserInfo>> _scannedSeries;
|
||||
|
||||
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger)
|
||||
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger, IArchiveService archiveService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_archiveService = archiveService;
|
||||
}
|
||||
|
||||
public void ScanLibraries()
|
||||
@ -60,6 +58,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) =>
|
||||
{
|
||||
@ -87,10 +91,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
|
||||
@ -154,7 +160,7 @@ namespace API.Services
|
||||
{
|
||||
if (info.Series == string.Empty) return;
|
||||
|
||||
_scannedSeries.AddOrUpdate(info.Series, new List<ParserInfo>() {info}, (key, oldValue) =>
|
||||
_scannedSeries.AddOrUpdate(info.Series, new List<ParserInfo>() {info}, (_, oldValue) =>
|
||||
{
|
||||
oldValue ??= new List<ParserInfo>();
|
||||
if (!oldValue.Contains(info))
|
||||
@ -187,10 +193,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())
|
||||
@ -212,191 +218,125 @@ 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 ? GetNumberOfPagesFromArchive(info.FullFilePath): 1
|
||||
NumberOfPages = info.Format == MangaFormat.Archive ? _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath): 1
|
||||
};
|
||||
}
|
||||
|
||||
private int MinimumNumberFromRange(string range)
|
||||
|
||||
private bool ShouldFindCoverImage(bool forceUpdate, byte[] coverImage)
|
||||
{
|
||||
var tokens = range.Split("-");
|
||||
return Int32.Parse(tokens.Length >= 1 ? tokens[0] : range);
|
||||
return forceUpdate || coverImage == null || !coverImage.Any();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
/// <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))
|
||||
{
|
||||
chapter.Files ??= new List<MangaFile>();
|
||||
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 existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
|
||||
if (existingVolume != null)
|
||||
var volume = (existingVolumes.SingleOrDefault(v => v.Name == info.Volumes) ??
|
||||
volumes.SingleOrDefault(v => v.Name == info.Volumes)) ?? new Volume
|
||||
{
|
||||
var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
||||
if (existingFile != null)
|
||||
{
|
||||
existingFile.Chapter = MinimumNumberFromRange(info.Chapters);
|
||||
existingFile.Format = info.Format;
|
||||
existingFile.NumberOfPages = GetNumberOfPagesFromArchive(info.FullFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (info.Format == MangaFormat.Archive)
|
||||
{
|
||||
existingVolume.Files.Add(CreateMangaFile(info));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug($"Ignoring {info.Filename} as it is not an archive.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
volumes.Add(existingVolume);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create New Volume
|
||||
existingVolume = volumes.SingleOrDefault(v => v.Name == info.Volumes);
|
||||
if (existingVolume != null)
|
||||
{
|
||||
existingVolume.Files.Add(CreateMangaFile(info));
|
||||
}
|
||||
else
|
||||
{
|
||||
var vol = new Volume()
|
||||
{
|
||||
Name = info.Volumes,
|
||||
Number = MinimumNumberFromRange(info.Volumes),
|
||||
Files = new List<MangaFile>()
|
||||
{
|
||||
CreateMangaFile(info)
|
||||
}
|
||||
};
|
||||
volumes.Add(vol);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Adding volume {volumes.Last().Number} with File: {info.Filename}");
|
||||
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 (forceUpdate || volume.CoverImage == null || !volume.Files.Any())
|
||||
if (ShouldFindCoverImage(forceUpdate, volume.CoverImage))
|
||||
{
|
||||
var firstFile = volume.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||
if (firstFile != null) volume.CoverImage = GetCoverImage(firstFile.FilePath, true); // ZIPFILE
|
||||
// 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);
|
||||
}
|
||||
|
||||
volume.Pages = volume.Files.Sum(x => x.NumberOfPages);
|
||||
}
|
||||
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void ScanSeries(int libraryId, int seriesId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private int GetNumberOfPagesFromArchive(string archivePath)
|
||||
{
|
||||
if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath))
|
||||
{
|
||||
_logger.LogError($"Archive {archivePath} could not be found.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
_logger.LogDebug($"Getting Page numbers from {archivePath}");
|
||||
|
||||
using ZipArchive archive = ZipFile.OpenRead(archivePath); // ZIPFILE
|
||||
return archive.Entries.Count(e => Parser.Parser.IsImage(e.FullName));
|
||||
}
|
||||
|
||||
/// <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 byte[] GetCoverImage(string filepath, bool createThumbnail = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(filepath) || !File.Exists(filepath) || !Parser.Parser.IsArchive(filepath)) return Array.Empty<byte>();
|
||||
|
||||
_logger.LogDebug($"Extracting Cover image from {filepath}");
|
||||
using ZipArchive archive = ZipFile.OpenRead(filepath);
|
||||
if (!archive.HasFiles()) return Array.Empty<byte>();
|
||||
|
||||
var folder = archive.Entries.SingleOrDefault(x => Path.GetFileNameWithoutExtension(x.Name).ToLower() == "folder");
|
||||
var entries = archive.Entries.Where(x => Path.HasExtension(x.FullName) && Parser.Parser.IsImage(x.FullName)).OrderBy(x => x.FullName).ToList();
|
||||
ZipArchiveEntry entry;
|
||||
|
||||
if (folder != null)
|
||||
{
|
||||
entry = folder;
|
||||
} else if (!entries.Any())
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
else
|
||||
{
|
||||
entry = entries[0];
|
||||
}
|
||||
|
||||
|
||||
if (createThumbnail)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
var thumbnail = Image.ThumbnailStream(stream, 320);
|
||||
return thumbnail.WriteToBuffer(".jpg");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was a critical error and prevented thumbnail generation.");
|
||||
}
|
||||
}
|
||||
|
||||
return ExtractEntryToImage(entry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception when reading archive stream.");
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
using API.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Helpers.Converters;
|
||||
using API.Interfaces;
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -11,15 +14,26 @@ namespace API.Services
|
||||
private readonly IScannerService _scannerService;
|
||||
public BackgroundJobServer Client => new BackgroundJobServer();
|
||||
|
||||
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService)
|
||||
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService, IUnitOfWork unitOfWork)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_logger = logger;
|
||||
_scannerService = scannerService;
|
||||
|
||||
_logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis.");
|
||||
RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily);
|
||||
RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), Cron.Daily);
|
||||
var setting = Task.Run(() => unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Result;
|
||||
if (setting != null)
|
||||
{
|
||||
RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), () => CronConverter.ConvertToCronNotation(setting.Value));
|
||||
}
|
||||
else
|
||||
{
|
||||
RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily);
|
||||
RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), Cron.Daily);
|
||||
}
|
||||
|
||||
//JobStorage.Current.GetMonitoringApi().
|
||||
|
||||
}
|
||||
|
||||
public void ScanSeries(int libraryId, int seriesId)
|
||||
@ -34,9 +48,9 @@ namespace API.Services
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate));
|
||||
}
|
||||
|
||||
public void CleanupVolumes(int[] volumeIds)
|
||||
public void CleanupChapters(int[] chapterIds)
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupVolumes(volumeIds));
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -45,8 +46,6 @@ namespace API
|
||||
app.UseHangfireDashboard();
|
||||
}
|
||||
|
||||
//app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
// Ordering is important. Cors, authentication, authorization
|
||||
@ -57,7 +56,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 =>
|
||||
{
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data source=kavita.db",
|
||||
"DefaultConnection": "Data source=kavita.db"
|
||||
},
|
||||
"TokenKey": "super secret unguessable key",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft": "Error",
|
||||
"Microsoft.Hosting.Lifetime": "Error",
|
||||
"Hangfire": "Information"
|
||||
},
|
||||
"File": {
|
||||
|