Merge pull request #46 from Kareadita/feature/compress-overhaul
server settings
1
.gitignore
vendored
@ -450,3 +450,4 @@ appsettings.json
|
|||||||
/API/Hangfire-log.db
|
/API/Hangfire-log.db
|
||||||
cache/
|
cache/
|
||||||
/API/wwwroot/
|
/API/wwwroot/
|
||||||
|
/API/cache/
|
@ -25,7 +25,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Services\Test Data" />
|
<Folder Include="Services\Test Data\ArchiveService" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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 v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
|
||||||
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
|
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
|
||||||
[InlineData("Dorohedoro v12 (2013) (Digital) (LostNerevarine-Empire).cbz", "12")]
|
[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)
|
public void ParseVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, ParseVolume(filename));
|
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("Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip", "Ichinensei ni Nacchattara")]
|
||||||
[InlineData("Chrno_Crusade_Dragon_Age_All_Stars[AS].zip", "")]
|
[InlineData("Chrno_Crusade_Dragon_Age_All_Stars[AS].zip", "")]
|
||||||
[InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip", "Ichiban Ushiro no Daimaou")]
|
[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")]
|
//[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "Epigraph of the Closed Curve")]
|
||||||
public void ParseSeriesTest(string filename, string expected)
|
public void ParseSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
@ -102,6 +107,9 @@ namespace API.Tests
|
|||||||
[InlineData("Mujaki no Rakuen Vol12 ch76", "76")]
|
[InlineData("Mujaki no Rakuen Vol12 ch76", "76")]
|
||||||
[InlineData("Beelzebub_01_[Noodles].zip", "1")]
|
[InlineData("Beelzebub_01_[Noodles].zip", "1")]
|
||||||
[InlineData("Yumekui-Merry_DKThias_Chapter21.zip", "21")]
|
[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")]
|
//[InlineData("[Tempus Edax Rerum] Epigraph of the Closed Curve - Chapter 6.zip", "6")]
|
||||||
public void ParseChaptersTest(string filename, string expected)
|
public void ParseChaptersTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
@ -159,6 +167,16 @@ namespace API.Tests
|
|||||||
Assert.Equal(expected, ParseEdition(input));
|
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]
|
[Fact]
|
||||||
public void ParseInfoTest()
|
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", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||||
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
|
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
|
||||||
)]
|
)]
|
||||||
|
|
||||||
public void TestLogicalComparer(string[] input, string[] expected)
|
public void TestLogicalComparer(string[] input, string[] expected)
|
||||||
{
|
{
|
||||||
NumericComparer nc = new NumericComparer();
|
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;
|
if (string.IsNullOrEmpty(s2)) return -1;
|
||||||
|
|
||||||
//WE style, special case
|
//WE style, special case
|
||||||
bool sp1 = Char.IsLetterOrDigit(s1, 0);
|
var sp1 = Char.IsLetterOrDigit(s1, 0);
|
||||||
bool sp2 = Char.IsLetterOrDigit(s2, 0);
|
var sp2 = Char.IsLetterOrDigit(s2, 0);
|
||||||
if(sp1 && !sp2) return 1;
|
if(sp1 && !sp2) return 1;
|
||||||
if(!sp1 && sp2) return -1;
|
if(!sp1 && sp2) return -1;
|
||||||
|
|
||||||
int i1 = 0, i2 = 0; //current index
|
int i1 = 0, i2 = 0; //current index
|
||||||
while(true)
|
while(true)
|
||||||
{
|
{
|
||||||
bool c1 = Char.IsDigit(s1, i1);
|
var c1 = Char.IsDigit(s1, i1);
|
||||||
bool c2 = Char.IsDigit(s2, i2);
|
var c2 = Char.IsDigit(s2, i2);
|
||||||
int r; // temp result
|
int r; // temp result
|
||||||
if(!c1 && !c2)
|
if(!c1 && !c2)
|
||||||
{
|
{
|
||||||
|
@ -20,9 +20,5 @@ namespace API.Controllers
|
|||||||
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
||||||
return users.Count > 0;
|
return users.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,4 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using API.Services;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
|
@ -145,7 +145,7 @@ namespace API.Controllers
|
|||||||
[HttpPost("scan")]
|
[HttpPost("scan")]
|
||||||
public ActionResult Scan(int libraryId)
|
public ActionResult Scan(int libraryId)
|
||||||
{
|
{
|
||||||
_taskScheduler.ScanLibrary(libraryId, false);
|
_taskScheduler.ScanLibrary(libraryId);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,13 +177,13 @@ namespace API.Controllers
|
|||||||
var username = User.GetUsername();
|
var username = User.GetUsername();
|
||||||
_logger.LogInformation($"Library {libraryId} is being deleted by {username}.");
|
_logger.LogInformation($"Library {libraryId} is being deleted by {username}.");
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
|
||||||
var volumes = (await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(series.Select(x => x.Id).ToArray()))
|
var chapterIds =
|
||||||
.Select(x => x.Id).ToArray();
|
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(series.Select(x => x.Id).ToArray());
|
||||||
var result = await _unitOfWork.LibraryRepository.DeleteLibrary(libraryId);
|
var result = await _unitOfWork.LibraryRepository.DeleteLibrary(libraryId);
|
||||||
|
|
||||||
if (result && volumes.Any())
|
if (result && chapterIds.Any())
|
||||||
{
|
{
|
||||||
_taskScheduler.CleanupVolumes(volumes);
|
_taskScheduler.CleanupChapters(chapterIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
|
@ -28,24 +28,28 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("image")]
|
[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
|
// 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);
|
var file = await _directoryService.ReadImageAsync(path);
|
||||||
file.Page = page;
|
file.Page = page;
|
||||||
|
file.MangaFileName = mangaFile.FilePath;
|
||||||
|
|
||||||
return Ok(file);
|
return Ok(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("get-bookmark")]
|
[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());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (user.Progresses == null) return Ok(0);
|
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);
|
if (progress != null) return Ok(progress.PagesRead);
|
||||||
|
|
||||||
@ -56,10 +60,12 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
|
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
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>();
|
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)
|
if (userProgress == null)
|
||||||
{
|
{
|
||||||
@ -69,13 +75,14 @@ namespace API.Controllers
|
|||||||
PagesRead = bookmarkDto.PageNum,
|
PagesRead = bookmarkDto.PageNum,
|
||||||
VolumeId = bookmarkDto.VolumeId,
|
VolumeId = bookmarkDto.VolumeId,
|
||||||
SeriesId = bookmarkDto.SeriesId,
|
SeriesId = bookmarkDto.SeriesId,
|
||||||
|
ChapterId = bookmarkDto.ChapterId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
userProgress.PagesRead = bookmarkDto.PageNum;
|
userProgress.PagesRead = bookmarkDto.PageNum;
|
||||||
userProgress.SeriesId = bookmarkDto.SeriesId;
|
userProgress.SeriesId = bookmarkDto.SeriesId;
|
||||||
|
userProgress.VolumeId = bookmarkDto.VolumeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
@ -85,7 +92,6 @@ namespace API.Controllers
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return BadRequest("Could not save progress");
|
return BadRequest("Could not save progress");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
@ -36,17 +35,22 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
|
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
|
||||||
{
|
{
|
||||||
var username = User.GetUsername();
|
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}.");
|
_logger.LogInformation($"Series {seriesId} is being deleted by {username}.");
|
||||||
var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId);
|
var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId);
|
||||||
|
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
_taskScheduler.CleanupVolumes(volumes);
|
_taskScheduler.CleanupChapters(chapterIds);
|
||||||
}
|
}
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns All volumes for a series with progress information and Chapters
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("volumes")]
|
[HttpGet("volumes")]
|
||||||
public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId)
|
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));
|
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")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost("scan")]
|
[HttpPost("scan")]
|
||||||
public ActionResult Scan(int libraryId, int seriesId)
|
public ActionResult Scan(int libraryId, int seriesId)
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
using System.IO;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
using API.Helpers.Converters;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Services;
|
|
||||||
using AutoMapper;
|
|
||||||
using AutoMapper.QueryableExtensions;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
@ -19,29 +15,24 @@ namespace API.Controllers
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class SettingsController : BaseApiController
|
public class SettingsController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly DataContext _dataContext;
|
|
||||||
private readonly ILogger<SettingsController> _logger;
|
private readonly ILogger<SettingsController> _logger;
|
||||||
private readonly IMapper _mapper;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ITaskScheduler _taskScheduler;
|
|
||||||
|
|
||||||
public SettingsController(DataContext dataContext, ILogger<SettingsController> logger, IMapper mapper, ITaskScheduler taskScheduler)
|
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork)
|
||||||
{
|
{
|
||||||
_dataContext = dataContext;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mapper = mapper;
|
_unitOfWork = unitOfWork;
|
||||||
_taskScheduler = taskScheduler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||||
{
|
{
|
||||||
var settings = await _dataContext.ServerSetting.Select(x => x).ToListAsync();
|
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
|
||||||
return _mapper.Map<ServerSettingDto>(settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
public async Task<ActionResult> UpdateSettings(ServerSettingDto updateSettingsDto)
|
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"{User.GetUsername()} is updating Server Settings");
|
_logger.LogInformation($"{User.GetUsername()} is updating Server Settings");
|
||||||
|
|
||||||
@ -54,13 +45,39 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
return BadRequest("Directory does not exist or is not accessible.");
|
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.
|
|
||||||
|
|
||||||
|
// We do not allow CacheDirectory changes, so we will ignore.
|
||||||
|
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||||
|
|
||||||
//_dataContext.ServerSetting.Update
|
foreach (var setting in currentSettings)
|
||||||
return BadRequest("Not Implemented");
|
{
|
||||||
|
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 int Height { get; init; }
|
||||||
public string Format { get; init; }
|
public string Format { get; init; }
|
||||||
public byte[] Content { 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 class ServerSettingDto
|
||||||
{
|
{
|
||||||
public string CacheDirectory { get; set; }
|
public string CacheDirectory { get; set; }
|
||||||
// public string Kind { get; init; }
|
public string TaskScan { get; set; }
|
||||||
// public string Value { get; init; }
|
public string LoggingLevel { get; set; }
|
||||||
|
public string TaskBackup { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
{
|
{
|
||||||
public class VolumeDto
|
public class VolumeDto
|
||||||
@ -9,5 +11,6 @@ namespace API.DTOs
|
|||||||
public byte[] CoverImage { get; set; }
|
public byte[] CoverImage { get; set; }
|
||||||
public int Pages { get; set; }
|
public int Pages { get; set; }
|
||||||
public int PagesRead { get; set; }
|
public int PagesRead { get; set; }
|
||||||
|
public ICollection<ChapterDto> Chapters { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,6 +3,7 @@
|
|||||||
public class BookmarkDto
|
public class BookmarkDto
|
||||||
{
|
{
|
||||||
public int VolumeId { get; init; }
|
public int VolumeId { get; init; }
|
||||||
|
public int ChapterId { get; init; }
|
||||||
public int PageNum { get; init; }
|
public int PageNum { get; init; }
|
||||||
public int SeriesId { get; init; }
|
public int SeriesId { get; init; }
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,11 @@ namespace API.Data
|
|||||||
}
|
}
|
||||||
public DbSet<Library> Library { get; set; }
|
public DbSet<Library> Library { get; set; }
|
||||||
public DbSet<Series> Series { get; set; }
|
public DbSet<Series> Series { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Chapter> Chapter { get; set; }
|
||||||
public DbSet<Volume> Volume { get; set; }
|
public DbSet<Volume> Volume { get; set; }
|
||||||
public DbSet<AppUser> AppUser { get; set; }
|
public DbSet<AppUser> AppUser { get; set; }
|
||||||
|
public DbSet<MangaFile> MangaFile { get; set; }
|
||||||
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
|
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
|
||||||
public DbSet<AppUserRating> AppUserRating { get; set; }
|
public DbSet<AppUserRating> AppUserRating { get; set; }
|
||||||
public DbSet<ServerSetting> ServerSetting { get; set; }
|
public DbSet<ServerSetting> ServerSetting { get; set; }
|
||||||
@ -30,10 +33,6 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
// builder.Entity<ServerSetting>()
|
|
||||||
// .HasAlternateKey(s => s.Key)
|
|
||||||
// .HasName("AlternateKey_Key");
|
|
||||||
|
|
||||||
builder.Entity<AppUser>()
|
builder.Entity<AppUser>()
|
||||||
.HasMany(ur => ur.UserRoles)
|
.HasMany(ur => ur.UserRoles)
|
||||||
.WithOne(u => u.User)
|
.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")
|
b.Property<int>("AppUserId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChapterId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("PagesRead")
|
b.Property<int>("PagesRead")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@ -183,12 +186,49 @@ namespace API.Data.Migrations
|
|||||||
b.ToTable("AspNetUserRoles");
|
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 =>
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastScanned")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("LibraryId")
|
b.Property<int>("LibraryId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@ -234,7 +274,7 @@ namespace API.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("Chapter")
|
b.Property<int>("ChapterId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("FilePath")
|
b.Property<string>("FilePath")
|
||||||
@ -246,12 +286,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("NumberOfPages")
|
b.Property<int>("NumberOfPages")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("VolumeId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("VolumeId");
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
b.ToTable("MangaFile");
|
b.ToTable("MangaFile");
|
||||||
});
|
});
|
||||||
@ -298,8 +335,8 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<int>("Key")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<uint>("RowVersion")
|
b.Property<uint>("RowVersion")
|
||||||
.IsConcurrencyToken()
|
.IsConcurrencyToken()
|
||||||
@ -325,6 +362,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<DateTime>("Created")
|
b.Property<DateTime>("Created")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpecial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTime>("LastModified")
|
b.Property<DateTime>("LastModified")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -487,6 +527,17 @@ namespace API.Data.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.Library", "Library")
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
@ -500,13 +551,13 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.Volume", "Volume")
|
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||||
.WithMany("Files")
|
.WithMany("Files")
|
||||||
.HasForeignKey("VolumeId")
|
.HasForeignKey("ChapterId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("Volume");
|
b.Navigation("Chapter");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Series", b =>
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
@ -596,6 +647,11 @@ namespace API.Data.Migrations
|
|||||||
b.Navigation("UserRoles");
|
b.Navigation("UserRoles");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Library", b =>
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Folders");
|
b.Navigation("Folders");
|
||||||
@ -610,7 +666,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Files");
|
b.Navigation("Chapters");
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
@ -31,20 +30,24 @@ namespace API.Data
|
|||||||
|
|
||||||
public static async Task SeedSettings(DataContext context)
|
public static async Task SeedSettings(DataContext context)
|
||||||
{
|
{
|
||||||
// NOTE: This needs to check if settings already exists before inserting.
|
context.Database.EnsureCreated();
|
||||||
// IList<ServerSetting> defaultSettings = new List<ServerSetting>()
|
|
||||||
// {
|
IList<ServerSetting> defaultSettings = new List<ServerSetting>()
|
||||||
// new ServerSetting() {Key = "CacheDirectory", Value = CacheService.CacheDirectory}
|
{
|
||||||
// };
|
new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory},
|
||||||
//
|
new () {Key = ServerSettingKey.TaskScan, Value = "daily"}
|
||||||
// await context.ServerSetting.AddRangeAsync(defaultSettings);
|
};
|
||||||
// await context.SaveChangesAsync();
|
|
||||||
// await context.ServerSetting.AddAsync(new ServerSetting
|
foreach (var defaultSetting in defaultSettings)
|
||||||
// {
|
{
|
||||||
// CacheDirectory = CacheService.CacheDirectory
|
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
|
||||||
// });
|
if (existing == null)
|
||||||
//
|
{
|
||||||
// await context.SaveChangesAsync();
|
context.ServerSetting.Add(defaultSetting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -82,6 +82,7 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
var volumes = await _context.Volume
|
var volumes = await _context.Volume
|
||||||
.Where(vol => vol.SeriesId == seriesId)
|
.Where(vol => vol.SeriesId == seriesId)
|
||||||
|
.Include(vol => vol.Chapters)
|
||||||
.OrderBy(volume => volume.Number)
|
.OrderBy(volume => volume.Number)
|
||||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@ -98,7 +99,8 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
return _context.Volume
|
return _context.Volume
|
||||||
.Where(vol => vol.SeriesId == seriesId)
|
.Where(vol => vol.SeriesId == seriesId)
|
||||||
.Include(vol => vol.Files)
|
.Include(vol => vol.Chapters)
|
||||||
|
.ThenInclude(c => c.Files)
|
||||||
.OrderBy(vol => vol.Number)
|
.OrderBy(vol => vol.Number)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@ -118,7 +120,8 @@ namespace API.Data
|
|||||||
public async Task<Volume> GetVolumeAsync(int volumeId)
|
public async Task<Volume> GetVolumeAsync(int volumeId)
|
||||||
{
|
{
|
||||||
return await _context.Volume
|
return await _context.Volume
|
||||||
.Include(vol => vol.Files)
|
.Include(vol => vol.Chapters)
|
||||||
|
.ThenInclude(c => c.Files)
|
||||||
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
|
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +129,8 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
var volume = await _context.Volume
|
var volume = await _context.Volume
|
||||||
.Where(vol => vol.Id == volumeId)
|
.Where(vol => vol.Id == volumeId)
|
||||||
.Include(vol => vol.Files)
|
.Include(vol => vol.Chapters)
|
||||||
|
.ThenInclude(c => c.Files)
|
||||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||||
.SingleAsync(vol => vol.Id == volumeId);
|
.SingleAsync(vol => vol.Id == volumeId);
|
||||||
|
|
||||||
@ -169,6 +173,30 @@ namespace API.Data
|
|||||||
.SingleOrDefaultAsync();
|
.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)
|
private async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
|
||||||
{
|
{
|
||||||
var userProgress = await _context.AppUserProgresses
|
var userProgress = await _context.AppUserProgresses
|
||||||
@ -197,6 +225,11 @@ namespace API.Data
|
|||||||
|
|
||||||
foreach (var v in volumes)
|
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);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,10 @@ namespace API.Data
|
|||||||
public IUserRepository UserRepository => new UserRepository(_context, _userManager);
|
public IUserRepository UserRepository => new UserRepository(_context, _userManager);
|
||||||
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
|
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()
|
public async Task<bool> Complete()
|
||||||
{
|
{
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
|
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
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public class AppUserProgress
|
public class AppUserProgress
|
||||||
{
|
{
|
||||||
@ -11,6 +11,8 @@ namespace API.Entities
|
|||||||
public int VolumeId { get; set; }
|
public int VolumeId { get; set; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
|
|
||||||
|
public int ChapterId { get; set; }
|
||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
public AppUser AppUser { get; set; }
|
public AppUser AppUser { get; set; }
|
||||||
public int AppUserId { 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
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
public class FolderPath
|
public class FolderPath
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Path { 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 Library Library { get; set; }
|
||||||
public int LibraryId { get; set; }
|
public int LibraryId { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -9,18 +9,14 @@ namespace API.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string FilePath { get; set; }
|
public string FilePath { get; set; }
|
||||||
/// <summary>
|
/// <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
|
/// Number of pages for the given file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int NumberOfPages { get; set; }
|
public int NumberOfPages { get; set; }
|
||||||
public MangaFormat Format { get; set; }
|
public MangaFormat Format { get; set; }
|
||||||
|
|
||||||
// Relationship Mapping
|
// Relationship Mapping
|
||||||
public Volume Volume { get; set; }
|
public Chapter Chapter { get; set; }
|
||||||
public int VolumeId { get; set; }
|
public int ChapterId { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,14 +12,14 @@ namespace API.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
/// <summary>
|
/// <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.
|
/// The name used to sort the Series. By default, will be the same as Name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SortName { get; set; }
|
public string SortName { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Original Name on disk. Not exposed to UI.
|
||||||
|
/// </summary>
|
||||||
|
public string OriginalName { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// Summary information related to the Series
|
/// Summary information related to the Series
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Summary { get; set; }
|
public string Summary { get; set; }
|
||||||
@ -27,7 +27,7 @@ namespace API.Entities
|
|||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
public byte[] CoverImage { get; set; }
|
public byte[] CoverImage { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sum of all Volume pages
|
/// Sum of all Volume page counts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Pages { get; set; }
|
public int Pages { get; set; }
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
public class ServerSetting : IHasConcurrencyToken
|
public class ServerSetting : IHasConcurrencyToken
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
public string Key { get; set; }
|
public ServerSettingKey Key { get; set; }
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
|
|
||||||
[ConcurrencyCheck]
|
[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,12 +9,17 @@ namespace API.Entities
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public int Number { 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 Created { get; set; }
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
public byte[] CoverImage { get; set; }
|
public byte[] CoverImage { get; set; }
|
||||||
public int Pages { 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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
|
@ -23,6 +23,7 @@ namespace API.Extensions
|
|||||||
services.AddScoped<ICacheService, CacheService>();
|
services.AddScoped<ICacheService, CacheService>();
|
||||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
services.AddScoped<IScannerService, ScannerService>();
|
services.AddScoped<IScannerService, ScannerService>();
|
||||||
|
services.AddScoped<IArchiveService, ArchiveService>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System.IO;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace API.Extensions
|
namespace API.Extensions
|
||||||
{
|
{
|
||||||
@ -51,7 +49,7 @@ namespace API.Extensions
|
|||||||
if (file.Directory == null) continue;
|
if (file.Directory == null) continue;
|
||||||
var newName = $"{file.Directory.Name}_{file.Name}";
|
var newName = $"{file.Directory.Name}_{file.Name}";
|
||||||
var newPath = Path.Join(root.FullName, newName);
|
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<Volume, VolumeDto>();
|
||||||
|
|
||||||
|
CreateMap<MangaFile, MangaFileDto>();
|
||||||
|
|
||||||
|
CreateMap<Chapter, ChapterDto>();
|
||||||
|
|
||||||
CreateMap<Series, SeriesDto>();
|
CreateMap<Series, SeriesDto>();
|
||||||
|
|
||||||
CreateMap<Library, LibraryDto>()
|
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)
|
public ServerSettingDto Convert(IEnumerable<ServerSetting> source, ServerSettingDto destination, ResolutionContext context)
|
||||||
{
|
{
|
||||||
destination = new ServerSettingDto();
|
destination ??= new ServerSettingDto();
|
||||||
foreach (var row in source)
|
foreach (var row in source)
|
||||||
{
|
{
|
||||||
switch (row.Key)
|
switch (row.Key)
|
||||||
{
|
{
|
||||||
case "CacheDirectory":
|
case ServerSettingKey.CacheDirectory:
|
||||||
destination.CacheDirectory = row.Value;
|
destination.CacheDirectory = row.Value;
|
||||||
break;
|
break;
|
||||||
default:
|
case ServerSettingKey.TaskScan:
|
||||||
|
destination.TaskScan = row.Value;
|
||||||
break;
|
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
|
public interface ICacheService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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).
|
/// cache operations (except cleanup).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="volumeId"></param>
|
/// <param name="chapterId"></param>
|
||||||
/// <returns>Volume for the passed volumeId. Side-effect from ensuring cache.</returns>
|
/// <returns>Chapter for the passed chapterId. Side-effect from ensuring cache.</returns>
|
||||||
Task<Volume> Ensure(int volumeId);
|
Task<Chapter> Ensure(int chapterId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears cache directory of all folders and files.
|
/// Clears cache directory of all folders and files.
|
||||||
@ -21,16 +21,18 @@ namespace API.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears cache directory of all volumes. This can be invoked from deleting a library or a series.
|
/// Clears cache directory of all volumes. This can be invoked from deleting a library or a series.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="volumeIds">Volumes that belong to that library. Assume the library might have been deleted before this invocation.</param>
|
/// <param name="chapterIds">Volumes that belong to that library. Assume the library might have been deleted before this invocation.</param>
|
||||||
void CleanupVolumes(int[] volumeIds);
|
void CleanupChapters(int[] chapterIds);
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the absolute path of a cached page.
|
/// Returns the absolute path of a cached page.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="volume"></param>
|
/// <param name="chapter">Chapter entity with Files populated.</param>
|
||||||
/// <param name="page">Page number to look for</param>
|
/// <param name="page">Page number to look for</param>
|
||||||
/// <returns></returns>
|
/// <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);
|
IEnumerable<string> ListDirectory(string rootPath);
|
||||||
|
|
||||||
Task<ImageDto> ReadImageAsync(string imagePath);
|
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;
|
namespace API.Interfaces
|
||||||
using API.DTOs;
|
|
||||||
|
|
||||||
namespace API.Interfaces
|
|
||||||
{
|
{
|
||||||
public interface IScannerService
|
public interface IScannerService
|
||||||
{
|
{
|
||||||
|
@ -16,14 +16,12 @@ namespace API.Interfaces
|
|||||||
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId);
|
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId);
|
||||||
IEnumerable<Volume> GetVolumes(int seriesId);
|
IEnumerable<Volume> GetVolumes(int seriesId);
|
||||||
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
||||||
|
|
||||||
Task<Volume> GetVolumeAsync(int volumeId);
|
Task<Volume> GetVolumeAsync(int volumeId);
|
||||||
Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId);
|
Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId);
|
||||||
|
|
||||||
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds);
|
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds);
|
||||||
Task<bool> DeleteSeriesAsync(int seriesId);
|
Task<bool> DeleteSeriesAsync(int seriesId);
|
||||||
Task<Volume> GetVolumeByIdAsync(int volumeId);
|
Task<Volume> GetVolumeByIdAsync(int volumeId);
|
||||||
Task<Series> GetSeriesByIdAsync(int seriesId);
|
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
|
public interface ITaskScheduler
|
||||||
{
|
{
|
||||||
void ScanLibrary(int libraryId, bool forceUpdate = false);
|
void ScanLibrary(int libraryId, bool forceUpdate = false);
|
||||||
void CleanupVolumes(int[] volumeIds);
|
void CleanupChapters(int[] chapterIds);
|
||||||
void ScanSeries(int libraryId, int seriesId);
|
void ScanSeries(int libraryId, int seriesId);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,8 @@ namespace API.Interfaces
|
|||||||
ISeriesRepository SeriesRepository { get; }
|
ISeriesRepository SeriesRepository { get; }
|
||||||
IUserRepository UserRepository { get; }
|
IUserRepository UserRepository { get; }
|
||||||
ILibraryRepository LibraryRepository { get; }
|
ILibraryRepository LibraryRepository { get; }
|
||||||
|
IVolumeRepository VolumeRepository { get; }
|
||||||
|
ISettingsRepository SettingsRepository { get; }
|
||||||
Task<bool> Complete();
|
Task<bool> Complete();
|
||||||
bool HasChanges();
|
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;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
|
||||||
@ -9,6 +11,8 @@ namespace API.Parser
|
|||||||
{
|
{
|
||||||
public static readonly string MangaFileExtensions = @"\.cbz|\.zip"; // |\.rar|\.cbr
|
public static readonly string MangaFileExtensions = @"\.cbz|\.zip"; // |\.rar|\.cbr
|
||||||
public static readonly string ImageFileExtensions = @"\.png|\.jpeg|\.jpg|\.gif";
|
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
|
//?: is a non-capturing group in C#, else anything in () will be a group
|
||||||
private static readonly Regex[] MangaVolumeRegex = new[]
|
private static readonly Regex[] MangaVolumeRegex = new[]
|
||||||
@ -125,7 +129,7 @@ namespace API.Parser
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
// Beelzebub_01_[Noodles].zip
|
// Beelzebub_01_[Noodles].zip
|
||||||
new Regex(
|
new Regex(
|
||||||
@"^((?!v|vo|vol|Volume).)*( |_)(?<Chapter>\.?\d+)( |_)",
|
@"^((?!v|vo|vol|Volume).)*( |_)(?<Chapter>\.?\d+)( |_|\[|\()",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
// Yumekui-Merry_DKThias_Chapter21.zip
|
// Yumekui-Merry_DKThias_Chapter21.zip
|
||||||
new Regex(
|
new Regex(
|
||||||
@ -168,7 +172,7 @@ namespace API.Parser
|
|||||||
/// <param name="filePath"></param>
|
/// <param name="filePath"></param>
|
||||||
/// <param name="rootPath">Root folder</param>
|
/// <param name="rootPath">Root folder</param>
|
||||||
/// <returns><see cref="ParserInfo"/> or null if Series was empty</returns>
|
/// <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 fileName = Path.GetFileName(filePath);
|
||||||
var directoryName = (new FileInfo(filePath)).Directory?.Name;
|
var directoryName = (new FileInfo(filePath)).Directory?.Name;
|
||||||
@ -387,13 +391,19 @@ namespace API.Parser
|
|||||||
public static bool IsArchive(string filePath)
|
public static bool IsArchive(string filePath)
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(filePath);
|
var fileInfo = new FileInfo(filePath);
|
||||||
return MangaFileExtensions.Contains(fileInfo.Extension);
|
return MangaFileRegex.IsMatch(fileInfo.Extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsImage(string filePath)
|
public static bool IsImage(string filePath)
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(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) =>
|
private static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
Host.CreateDefaultBuilder(args)
|
Host.CreateDefaultBuilder(args)
|
||||||
// .ConfigureLogging(logging =>
|
|
||||||
// {
|
|
||||||
// logging.ClearProviders();
|
|
||||||
// logging.AddConsole();
|
|
||||||
// })
|
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
{
|
{
|
||||||
webBuilder.UseStartup<Startup>();
|
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;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
@ -13,53 +12,51 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
public class CacheService : ICacheService
|
public class CacheService : ICacheService
|
||||||
{
|
{
|
||||||
private readonly IDirectoryService _directoryService;
|
|
||||||
private readonly ILogger<CacheService> _logger;
|
private readonly ILogger<CacheService> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IArchiveService _archiveService;
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
private readonly NumericComparer _numericComparer;
|
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;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_archiveService = archiveService;
|
||||||
|
_directoryService = directoryService;
|
||||||
_numericComparer = new NumericComparer();
|
_numericComparer = new NumericComparer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CacheDirectoryIsAccessible()
|
public void EnsureCacheDirectory()
|
||||||
{
|
{
|
||||||
_logger.LogDebug($"Checking if valid Cache directory: {CacheDirectory}");
|
_logger.LogDebug($"Checking if valid Cache directory: {CacheDirectory}");
|
||||||
var di = new DirectoryInfo(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);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Volume volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
|
||||||
foreach (var file in volume.Files)
|
|
||||||
{
|
|
||||||
var extractPath = GetVolumeCachePath(volumeId, file);
|
|
||||||
|
|
||||||
ExtractArchive(file.FilePath, extractPath);
|
foreach (var file in chapter.Files)
|
||||||
|
{
|
||||||
|
var extractPath = GetCachePath(chapterId);
|
||||||
|
_archiveService.ExtractArchive(file.FilePath, extractPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return volume;
|
return chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Cleanup()
|
public void Cleanup()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Performing cleanup of Cache directory");
|
_logger.LogInformation("Performing cleanup of Cache directory");
|
||||||
|
EnsureCacheDirectory();
|
||||||
if (!CacheDirectoryIsAccessible())
|
|
||||||
{
|
|
||||||
_logger.LogError($"Cache directory {CacheDirectory} is not accessible or does not exist.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DirectoryInfo di = new DirectoryInfo(CacheDirectory);
|
DirectoryInfo di = new DirectoryInfo(CacheDirectory);
|
||||||
|
|
||||||
@ -75,13 +72,13 @@ namespace API.Services
|
|||||||
_logger.LogInformation("Cache directory purged.");
|
_logger.LogInformation("Cache directory purged.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CleanupVolumes(int[] volumeIds)
|
public void CleanupChapters(int[] chapterIds)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"Running Cache cleanup on Volumes");
|
_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)
|
if (di.Exists)
|
||||||
{
|
{
|
||||||
di.Delete(true);
|
di.Delete(true);
|
||||||
@ -91,74 +88,37 @@ namespace API.Services
|
|||||||
_logger.LogInformation("Cache directory purged");
|
_logger.LogInformation("Cache directory purged");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts an archive to a temp cache directory. Returns path to new directory. If temp cache directory already exists,
|
/// Returns the cache path for a given Chapter. Should be cacheDirectory/{chapterId}/
|
||||||
/// 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>
|
/// </summary>
|
||||||
/// <param name="archivePath">A valid file to an archive file.</param>
|
/// <param name="chapterId"></param>
|
||||||
/// <param name="extractPath">Path to extract to</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private string ExtractArchive(string archivePath, string extractPath)
|
private string GetCachePath(int chapterId)
|
||||||
{
|
{
|
||||||
// NOTE: This is used by Cache Service
|
return Path.GetFullPath(Path.Join(CacheDirectory, $"{chapterId}/"));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(string path, MangaFile file)> GetCachedPagePath(Chapter chapter, int page)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
// Calculate what chapter the page belongs to
|
// Calculate what chapter the page belongs to
|
||||||
var pagesSoFar = 0;
|
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 path = GetCachePath(chapter.Id);
|
||||||
var files = DirectoryService.GetFiles(path);
|
var files = _directoryService.GetFiles(path, Parser.Parser.ImageFileExtensions);
|
||||||
Array.Sort(files, _numericComparer);
|
Array.Sort(files, _numericComparer);
|
||||||
|
|
||||||
return files.ElementAt(page - pagesSoFar);
|
return (files.ElementAt(page - pagesSoFar), mangaFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
pagesSoFar += mangaFile.NumberOfPages;
|
pagesSoFar += mangaFile.NumberOfPages;
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
|
return ("", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,12 +8,19 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using NetVips;
|
using NetVips;
|
||||||
|
|
||||||
namespace API.Services
|
namespace API.Services
|
||||||
{
|
{
|
||||||
public class DirectoryService : IDirectoryService
|
public class DirectoryService : IDirectoryService
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<DirectoryService> _logger;
|
||||||
|
|
||||||
|
public DirectoryService(ILogger<DirectoryService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a set of regex search criteria, get files in the given path.
|
/// 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)));
|
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>();
|
if (searchPatternExpression != string.Empty)
|
||||||
return Directory.GetFiles(path);
|
{
|
||||||
|
return GetFilesWithCertainExtensions(path, searchPatternExpression).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return !Directory.Exists(path) ? Array.Empty<string>() : Directory.GetFiles(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> ListDirectory(string rootPath)
|
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)))
|
.Where(dir => !(dir.Attributes.HasFlag(FileAttributes.Hidden) || dir.Attributes.HasFlag(FileAttributes.System)))
|
||||||
.Select(d => d.Name).ToImmutableList();
|
.Select(d => d.Name).ToImmutableList();
|
||||||
|
|
||||||
|
|
||||||
return dirs;
|
return dirs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ImageDto> ReadImageAsync(string imagePath)
|
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);
|
using var image = Image.NewFromFile(imagePath);
|
||||||
|
|
||||||
return new ImageDto
|
return new ImageDto
|
||||||
@ -63,7 +78,7 @@ namespace API.Services
|
|||||||
FullPath = Path.GetFullPath(imagePath),
|
FullPath = Path.GetFullPath(imagePath),
|
||||||
Width = image.Width,
|
Width = image.Width,
|
||||||
Height = image.Height,
|
Height = image.Height,
|
||||||
Format = image.Format
|
Format = image.Format,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,16 +4,12 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NetVips;
|
|
||||||
|
|
||||||
namespace API.Services
|
namespace API.Services
|
||||||
{
|
{
|
||||||
@ -21,12 +17,14 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ILogger<ScannerService> _logger;
|
private readonly ILogger<ScannerService> _logger;
|
||||||
|
private readonly IArchiveService _archiveService;
|
||||||
private ConcurrentDictionary<string, List<ParserInfo>> _scannedSeries;
|
private ConcurrentDictionary<string, List<ParserInfo>> _scannedSeries;
|
||||||
|
|
||||||
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger)
|
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger, IArchiveService archiveService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_archiveService = archiveService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ScanLibraries()
|
public void ScanLibraries()
|
||||||
@ -60,6 +58,12 @@ namespace API.Services
|
|||||||
var totalFiles = 0;
|
var totalFiles = 0;
|
||||||
foreach (var folderPath in library.Folders)
|
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 {
|
try {
|
||||||
totalFiles += DirectoryService.TraverseTreeParallelForEach(folderPath.Path, (f) =>
|
totalFiles += DirectoryService.TraverseTreeParallelForEach(folderPath.Path, (f) =>
|
||||||
{
|
{
|
||||||
@ -87,10 +91,12 @@ namespace API.Services
|
|||||||
// Remove series that are no longer on disk
|
// Remove series that are no longer on disk
|
||||||
RemoveSeriesNotOnDisk(allSeries, series, library);
|
RemoveSeriesNotOnDisk(allSeries, series, library);
|
||||||
|
|
||||||
|
foreach (var folder in library.Folders) folder.LastScanned = DateTime.Now;
|
||||||
_unitOfWork.LibraryRepository.Update(library);
|
_unitOfWork.LibraryRepository.Update(library);
|
||||||
|
|
||||||
if (Task.Run(() => _unitOfWork.Complete()).Result)
|
if (Task.Run(() => _unitOfWork.Complete()).Result)
|
||||||
{
|
{
|
||||||
|
|
||||||
_logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series.");
|
_logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -154,7 +160,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
if (info.Series == string.Empty) return;
|
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>();
|
oldValue ??= new List<ParserInfo>();
|
||||||
if (!oldValue.Contains(info))
|
if (!oldValue.Contains(info))
|
||||||
@ -187,10 +193,10 @@ namespace API.Services
|
|||||||
|
|
||||||
private Series UpdateSeries(Series series, ParserInfo[] infos, bool forceUpdate)
|
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.Volumes = volumes;
|
||||||
series.Pages = volumes.Sum(v => v.Pages);
|
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);
|
var firstCover = volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0);
|
||||||
if (firstCover == null && volumes.Any())
|
if (firstCover == null && volumes.Any())
|
||||||
@ -212,97 +218,113 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
_logger.LogDebug($"Creating File Entry for {info.FullFilePath}");
|
_logger.LogDebug($"Creating File Entry for {info.FullFilePath}");
|
||||||
|
|
||||||
int.TryParse(info.Chapters, out var chapter);
|
|
||||||
_logger.LogDebug($"Found Chapter: {chapter}");
|
|
||||||
return new MangaFile()
|
return new MangaFile()
|
||||||
{
|
{
|
||||||
FilePath = info.FullFilePath,
|
FilePath = info.FullFilePath,
|
||||||
Chapter = chapter,
|
|
||||||
Format = info.Format,
|
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 forceUpdate || coverImage == null || !coverImage.Any();
|
||||||
return Int32.Parse(tokens.Length >= 1 ? tokens[0] : range);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates or Updates volumes for a given series
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="series">Series wanting to be updated</param>
|
/// <param name="volume"></param>
|
||||||
/// <param name="infos">Parser info</param>
|
/// <param name="infos"></param>
|
||||||
/// <param name="forceUpdate">Forces metadata update (cover image) even if it's already been set.</param>
|
/// <param name="forceUpdate"></param>
|
||||||
/// <returns>Updated Volumes for given series</returns>
|
/// <returns></returns>
|
||||||
private ICollection<Volume> UpdateVolumes(Series series, ParserInfo[] infos, bool forceUpdate)
|
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>();
|
ICollection<Volume> volumes = new List<Volume>();
|
||||||
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
|
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
|
||||||
|
|
||||||
foreach (var info in infos)
|
foreach (var info in infos)
|
||||||
{
|
{
|
||||||
var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
|
var volume = (existingVolumes.SingleOrDefault(v => v.Name == info.Volumes) ??
|
||||||
if (existingVolume != null)
|
volumes.SingleOrDefault(v => v.Name == info.Volumes)) ?? new Volume
|
||||||
{
|
{
|
||||||
var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
Name = info.Volumes,
|
||||||
if (existingFile != null)
|
Number = Parser.Parser.MinimumNumberFromRange(info.Volumes),
|
||||||
{
|
};
|
||||||
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);
|
var chapters = UpdateChapters(volume, infos.Where(pi => pi.Volumes == volume.Name).ToArray(), forceUpdate);
|
||||||
}
|
volume.Chapters = chapters;
|
||||||
else
|
volume.Pages = chapters.Sum(c => c.Pages);
|
||||||
{
|
volumes.Add(volume);
|
||||||
// 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}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var volume in volumes)
|
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();
|
// TODO: Create a custom sorter for Chapters so it's consistent across the application
|
||||||
if (firstFile != null) volume.CoverImage = GetCoverImage(firstFile.FilePath, true); // ZIPFILE
|
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;
|
return volumes;
|
||||||
@ -310,93 +332,11 @@ namespace API.Services
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void ScanSeries(int libraryId, int seriesId)
|
public void ScanSeries(int libraryId, int seriesId)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,7 @@
|
|||||||
using API.Interfaces;
|
using System.Threading.Tasks;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Helpers.Converters;
|
||||||
|
using API.Interfaces;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@ -11,15 +14,26 @@ namespace API.Services
|
|||||||
private readonly IScannerService _scannerService;
|
private readonly IScannerService _scannerService;
|
||||||
public BackgroundJobServer Client => new BackgroundJobServer();
|
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;
|
_cacheService = cacheService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_scannerService = scannerService;
|
_scannerService = scannerService;
|
||||||
|
|
||||||
_logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis.");
|
_logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis.");
|
||||||
RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily);
|
var setting = Task.Run(() => unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Result;
|
||||||
RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), Cron.Daily);
|
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)
|
public void ScanSeries(int libraryId, int seriesId)
|
||||||
@ -34,9 +48,9 @@ namespace API.Services
|
|||||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate));
|
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 Hangfire;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@ -45,8 +46,6 @@ namespace API
|
|||||||
app.UseHangfireDashboard();
|
app.UseHangfireDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
//app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
// Ordering is important. Cors, authentication, authorization
|
// Ordering is important. Cors, authentication, authorization
|
||||||
@ -57,7 +56,11 @@ namespace API
|
|||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
|
||||||
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
{
|
||||||
|
ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default
|
||||||
|
});
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Data source=kavita.db",
|
"DefaultConnection": "Data source=kavita.db"
|
||||||
},
|
},
|
||||||
"TokenKey": "super secret unguessable key",
|
"TokenKey": "super secret unguessable key",
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Debug",
|
||||||
"Microsoft": "Information",
|
"Microsoft": "Error",
|
||||||
"Microsoft.Hosting.Lifetime": "Information",
|
"Microsoft.Hosting.Lifetime": "Error",
|
||||||
"Hangfire": "Information"
|
"Hangfire": "Information"
|
||||||
},
|
},
|
||||||
"File": {
|
"File": {
|
||||||
|