mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Merge pull request #42 from Kareadita/feature/server-settings
server settings + Lots of Regex
This commit is contained in:
commit
e7f2baaa94
35
.gitignore
vendored
35
.gitignore
vendored
@ -449,37 +449,4 @@ appsettings.json
|
|||||||
/API/Hangfire.db
|
/API/Hangfire.db
|
||||||
/API/Hangfire-log.db
|
/API/Hangfire-log.db
|
||||||
cache/
|
cache/
|
||||||
/API/wwwroot/assets/images/image-placeholder.jpg
|
/API/wwwroot/
|
||||||
/API/wwwroot/assets/images/mock-cover.jpg
|
|
||||||
/API/wwwroot/assets/images/preset-light.png
|
|
||||||
/API/wwwroot/assets/themes/plex/_bootswatch.scss
|
|
||||||
/API/wwwroot/assets/themes/plex/_variables.scss
|
|
||||||
/API/wwwroot/admin-admin-module.js
|
|
||||||
/API/wwwroot/admin-admin-module.js.map
|
|
||||||
/API/wwwroot/fa-brands-400.eot
|
|
||||||
/API/wwwroot/fa-brands-400.svg
|
|
||||||
/API/wwwroot/fa-brands-400.ttf
|
|
||||||
/API/wwwroot/fa-brands-400.woff
|
|
||||||
/API/wwwroot/fa-brands-400.woff2
|
|
||||||
/API/wwwroot/fa-regular-400.eot
|
|
||||||
/API/wwwroot/fa-regular-400.svg
|
|
||||||
/API/wwwroot/fa-regular-400.ttf
|
|
||||||
/API/wwwroot/fa-regular-400.woff
|
|
||||||
/API/wwwroot/fa-regular-400.woff2
|
|
||||||
/API/wwwroot/fa-solid-900.eot
|
|
||||||
/API/wwwroot/fa-solid-900.svg
|
|
||||||
/API/wwwroot/fa-solid-900.ttf
|
|
||||||
/API/wwwroot/fa-solid-900.woff
|
|
||||||
/API/wwwroot/fa-solid-900.woff2
|
|
||||||
/API/wwwroot/favicon.ico
|
|
||||||
/API/wwwroot/index.html
|
|
||||||
/API/wwwroot/main.js
|
|
||||||
/API/wwwroot/main.js.map
|
|
||||||
/API/wwwroot/polyfills.js
|
|
||||||
/API/wwwroot/polyfills.js.map
|
|
||||||
/API/wwwroot/runtime.js
|
|
||||||
/API/wwwroot/runtime.js.map
|
|
||||||
/API/wwwroot/styles.css
|
|
||||||
/API/wwwroot/styles.css.map
|
|
||||||
/API/wwwroot/vendor.js
|
|
||||||
/API/wwwroot/vendor.js.map
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Parser;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using static API.Parser.Parser;
|
using static API.Parser.Parser;
|
||||||
|
|
||||||
@ -12,11 +15,24 @@ namespace API.Tests
|
|||||||
[InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", "1")]
|
[InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", "1")]
|
||||||
[InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", "1")]
|
[InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", "1")]
|
||||||
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1")]
|
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1")]
|
||||||
//[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "16-17")]
|
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "16-17")]
|
||||||
[InlineData("Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", "1")]
|
[InlineData("Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", "1")]
|
||||||
[InlineData("v001", "1")]
|
[InlineData("v001", "1")]
|
||||||
|
[InlineData("No Volume", "0")]
|
||||||
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
|
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
|
||||||
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1")]
|
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1")]
|
||||||
|
[InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")]
|
||||||
|
[InlineData("[WS]_Ichiban_Ushiro_no_Daimaou_v02_ch10.zip", "2")]
|
||||||
|
[InlineData("[xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]", "1")]
|
||||||
|
[InlineData("Tower Of God S01 014 (CBT) (digital).cbz", "1")]
|
||||||
|
[InlineData("Tenjou_Tenge_v17_c100[MT].zip", "17")]
|
||||||
|
[InlineData("Shimoneta - Manmaru Hen - c001-006 (v01) [Various].zip", "1")]
|
||||||
|
[InlineData("Future Diary v02 (2009) (Digital) (Viz).cbz", "2")]
|
||||||
|
[InlineData("Mujaki no Rakuen Vol12 ch76", "12")]
|
||||||
|
[InlineData("Ichinensei_ni_Nacchattara_v02_ch11_[Taruby]_v1.3.zip", "2")]
|
||||||
|
[InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
|
||||||
|
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
|
||||||
|
[InlineData("Dorohedoro v12 (2013) (Digital) (LostNerevarine-Empire).cbz", "12")]
|
||||||
public void ParseVolumeTest(string filename, string expected)
|
public void ParseVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, ParseVolume(filename));
|
Assert.Equal(expected, ParseVolume(filename));
|
||||||
@ -31,13 +47,39 @@ namespace API.Tests
|
|||||||
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "Gokukoku no Brynhildr")]
|
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "Gokukoku no Brynhildr")]
|
||||||
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "Dance in the Vampire Bund")]
|
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "Dance in the Vampire Bund")]
|
||||||
[InlineData("v001", "")]
|
[InlineData("v001", "")]
|
||||||
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "U12 (Under 12)")]
|
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "U12")]
|
||||||
[InlineData("Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)", "Akame ga KILL! ZERO")]
|
[InlineData("Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)", "Akame ga KILL! ZERO")]
|
||||||
|
[InlineData("APOSIMZ 017 (2018) (Digital) (danke-Empire).cbz", "APOSIMZ")]
|
||||||
|
[InlineData("Akiiro Bousou Biyori - 01.jpg", "Akiiro Bousou Biyori")]
|
||||||
|
[InlineData("Beelzebub_172_RHS.zip", "Beelzebub")]
|
||||||
|
[InlineData("Dr. STONE 136 (2020) (Digital) (LuCaZ).cbz", "Dr. STONE")]
|
||||||
|
[InlineData("Cynthia the Mission 29.rar", "Cynthia the Mission")]
|
||||||
|
[InlineData("Darling in the FranXX - Volume 01.cbz", "Darling in the FranXX")]
|
||||||
|
[InlineData("Darwin's Game - Volume 14 (F).cbz", "Darwin's Game")]
|
||||||
|
[InlineData("[BAA]_Darker_than_Black_c7.zip", "Darker than Black")]
|
||||||
|
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip", "Kedouin Makoto - Corpse Party Musume")]
|
||||||
|
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01", "Kedouin Makoto - Corpse Party Musume")]
|
||||||
|
[InlineData("[WS]_Ichiban_Ushiro_no_Daimaou_v02_ch10.zip", "Ichiban Ushiro no Daimaou")]
|
||||||
|
[InlineData("[xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]", "Kyochuu Rettou")]
|
||||||
|
[InlineData("Loose_Relation_Between_Wizard_and_Apprentice_c07[AN].zip", "Loose Relation Between Wizard and Apprentice")]
|
||||||
|
[InlineData("Tower Of God S01 014 (CBT) (digital).cbz", "Tower Of God")]
|
||||||
|
[InlineData("Tenjou_Tenge_c106[MT].zip", "Tenjou Tenge")]
|
||||||
|
[InlineData("Tenjou_Tenge_v17_c100[MT].zip", "Tenjou Tenge")]
|
||||||
|
[InlineData("Shimoneta - Manmaru Hen - c001-006 (v01) [Various].zip", "Shimoneta - Manmaru Hen")]
|
||||||
|
[InlineData("Future Diary v02 (2009) (Digital) (Viz).cbz", "Future Diary")]
|
||||||
|
[InlineData("Tonikaku Cawaii [Volume 11].cbz", "Tonikaku Cawaii")]
|
||||||
|
[InlineData("Mujaki no Rakuen Vol12 ch76", "Mujaki no Rakuen")]
|
||||||
|
[InlineData("Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans]", "Knights of Sidonia")]
|
||||||
|
[InlineData("Vol 1.cbz", "")]
|
||||||
|
[InlineData("Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip", "Ichinensei ni Nacchattara")]
|
||||||
|
[InlineData("Chrno_Crusade_Dragon_Age_All_Stars[AS].zip", "")]
|
||||||
|
[InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip", "Ichiban Ushiro no Daimaou")]
|
||||||
|
//[InlineData("[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)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, ParseSeries(filename));
|
Assert.Equal(expected, ParseSeries(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
|
[InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
|
||||||
[InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")]
|
[InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")]
|
||||||
@ -49,6 +91,18 @@ namespace API.Tests
|
|||||||
[InlineData("c001", "1")]
|
[InlineData("c001", "1")]
|
||||||
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "12")]
|
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "12")]
|
||||||
[InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")]
|
[InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")]
|
||||||
|
[InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")]
|
||||||
|
[InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")]
|
||||||
|
[InlineData("[WS]_Ichiban_Ushiro_no_Daimaou_v02_ch10.zip", "10")]
|
||||||
|
[InlineData("Loose_Relation_Between_Wizard_and_Apprentice_c07[AN].zip", "7")]
|
||||||
|
[InlineData("Tower Of God S01 014 (CBT) (digital).cbz", "14")]
|
||||||
|
[InlineData("Tenjou_Tenge_c106[MT].zip", "106")]
|
||||||
|
[InlineData("Tenjou_Tenge_v17_c100[MT].zip", "100")]
|
||||||
|
[InlineData("Shimoneta - Manmaru Hen - c001-006 (v01) [Various].zip", "1-6")]
|
||||||
|
[InlineData("Mujaki no Rakuen Vol12 ch76", "76")]
|
||||||
|
[InlineData("Beelzebub_01_[Noodles].zip", "1")]
|
||||||
|
[InlineData("Yumekui-Merry_DKThias_Chapter21.zip", "21")]
|
||||||
|
//[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)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, ParseChapter(filename));
|
Assert.Equal(expected, ParseChapter(filename));
|
||||||
@ -78,6 +132,7 @@ namespace API.Tests
|
|||||||
[InlineData("Hello_I_am_here ", "Hello I am here")]
|
[InlineData("Hello_I_am_here ", "Hello I am here")]
|
||||||
[InlineData("[ReleaseGroup] The Title", "The Title")]
|
[InlineData("[ReleaseGroup] The Title", "The Title")]
|
||||||
[InlineData("[ReleaseGroup]_The_Title", "The Title")]
|
[InlineData("[ReleaseGroup]_The_Title", "The Title")]
|
||||||
|
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", "Kasumi Otoko no Ko v1.1")]
|
||||||
public void CleanTitleTest(string input, string expected)
|
public void CleanTitleTest(string input, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, CleanTitle(input));
|
Assert.Equal(expected, CleanTitle(input));
|
||||||
@ -85,13 +140,108 @@ namespace API.Tests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("test.cbz", true)]
|
[InlineData("test.cbz", true)]
|
||||||
[InlineData("test.cbr", true)]
|
[InlineData("test.cbr", false)]
|
||||||
[InlineData("test.zip", true)]
|
[InlineData("test.zip", true)]
|
||||||
[InlineData("test.rar", true)]
|
[InlineData("test.rar", false)]
|
||||||
[InlineData("test.rar.!qb", false)]
|
[InlineData("test.rar.!qb", false)]
|
||||||
|
[InlineData("[shf-ma-khs-aqs]negi_pa_vol15007.jpg", false)]
|
||||||
public void IsArchiveTest(string input, bool expected)
|
public void IsArchiveTest(string input, bool expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, IsArchive(input));
|
Assert.Equal(expected, IsArchive(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Tenjou Tenge Omnibus", "Omnibus")]
|
||||||
|
[InlineData("Tenjou Tenge {Full Contact Edition}", "Full Contact Edition")]
|
||||||
|
[InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "Full Contact Edition")]
|
||||||
|
public void ParseEditionTest(string input, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, ParseEdition(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInfoTest()
|
||||||
|
{
|
||||||
|
const string rootPath = @"E:/Manga/";
|
||||||
|
var expected = new Dictionary<string, ParserInfo>();
|
||||||
|
var filepath = @"E:/Manga/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Mujaki no Rakuen", Volumes = "12",
|
||||||
|
Chapters = "76", Filename = "Mujaki no Rakuen Vol12 ch76.cbz", Format = MangaFormat.Archive,
|
||||||
|
FullFilePath = filepath
|
||||||
|
});
|
||||||
|
|
||||||
|
filepath = @"E:/Manga/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1",
|
||||||
|
Chapters = "0", Filename = "Vol 1.cbz", Format = MangaFormat.Archive,
|
||||||
|
FullFilePath = filepath
|
||||||
|
});
|
||||||
|
|
||||||
|
filepath = @"E:\Manga\Beelzebub\Beelzebub_01_[Noodles].zip";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Beelzebub", Volumes = "0",
|
||||||
|
Chapters = "1", Filename = "Beelzebub_01_[Noodles].zip", Format = MangaFormat.Archive,
|
||||||
|
FullFilePath = filepath
|
||||||
|
});
|
||||||
|
|
||||||
|
filepath = @"E:\Manga\Ichinensei ni Nacchattara\Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Ichinensei ni Nacchattara", Volumes = "1",
|
||||||
|
Chapters = "1", Filename = "Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip", Format = MangaFormat.Archive,
|
||||||
|
FullFilePath = filepath
|
||||||
|
});
|
||||||
|
|
||||||
|
filepath = @"E:\Manga\Tenjo Tenge (Color)\Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Tenjo Tenge", Volumes = "1", Edition = "Full Contact Edition",
|
||||||
|
Chapters = "0", Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive,
|
||||||
|
FullFilePath = filepath
|
||||||
|
});
|
||||||
|
|
||||||
|
filepath = @"E:\Manga\Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)\Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Akame ga KILL! ZERO", Volumes = "1", Edition = "",
|
||||||
|
Chapters = "0", Filename = "Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive,
|
||||||
|
FullFilePath = filepath
|
||||||
|
});
|
||||||
|
|
||||||
|
filepath = @"E:\Manga\Dorohedoro\Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Dorohedoro", Volumes = "1", Edition = "",
|
||||||
|
Chapters = "0", Filename = "Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", Format = MangaFormat.Archive,
|
||||||
|
FullFilePath = filepath
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var file in expected.Keys)
|
||||||
|
{
|
||||||
|
var expectedInfo = expected[file];
|
||||||
|
var actual = Parse(file, rootPath);
|
||||||
|
if (expectedInfo == null)
|
||||||
|
{
|
||||||
|
Assert.Null(actual);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
Assert.Equal(expectedInfo.Format, actual.Format);
|
||||||
|
Assert.Equal(expectedInfo.Series, actual.Series);
|
||||||
|
Assert.Equal(expectedInfo.Chapters, actual.Chapters);
|
||||||
|
Assert.Equal(expectedInfo.Volumes, actual.Volumes);
|
||||||
|
Assert.Equal(expectedInfo.Edition, actual.Edition);
|
||||||
|
Assert.Equal(expectedInfo.Filename, actual.Filename);
|
||||||
|
Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,21 +1,20 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using API.IO;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services
|
||||||
{
|
{
|
||||||
public class ImageProviderTest
|
public class ImageProviderTest
|
||||||
{
|
{
|
||||||
[Theory]
|
// [Theory]
|
||||||
[InlineData("v10.cbz", "v10.expected.jpg")]
|
// [InlineData("v10.cbz", "v10.expected.jpg")]
|
||||||
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.jpg")]
|
// [InlineData("v10 - with folder.cbz", "v10 - with folder.expected.jpg")]
|
||||||
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")]
|
// [InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")]
|
||||||
public void GetCoverImageTest(string inputFile, string expectedOutputFile)
|
// public void GetCoverImageTest(string inputFile, string expectedOutputFile)
|
||||||
{
|
// {
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageProvider");
|
// var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageProvider");
|
||||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
// var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||||
|
// // TODO: Implement this with ScannerService
|
||||||
Assert.Equal(expectedBytes, ImageProvider.GetCoverImage(Path.Join(testDirectory, inputFile)));
|
// //Assert.Equal(expectedBytes, ImageProvider.GetCoverImage(Path.Join(testDirectory, inputFile)));
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
20
API.Tests/Services/ScannerServiceTests.cs
Normal file
20
API.Tests/Services/ScannerServiceTests.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||||
<PackageReference Include="NetVips" Version="1.2.4" />
|
<PackageReference Include="NetVips" Version="1.2.4" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.10.5.1" />
|
<PackageReference Include="NetVips.Native" Version="8.10.5.1" />
|
||||||
|
<PackageReference Include="NReco.Logging.File" Version="1.1.1" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.16.0.25740">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.16.0.25740">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -36,6 +39,21 @@ namespace API.Controllers
|
|||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpPost("reset-password")]
|
||||||
|
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"{User.GetUsername()} is changing {resetPasswordDto.UserName}'s password.");
|
||||||
|
var user = await _userManager.Users.SingleAsync(x => x.UserName == resetPasswordDto.UserName);
|
||||||
|
var result = await _userManager.RemovePasswordAsync(user);
|
||||||
|
if (!result.Succeeded) return BadRequest("Unable to update password");
|
||||||
|
|
||||||
|
result = await _userManager.AddPasswordAsync(user, resetPasswordDto.Password);
|
||||||
|
if (!result.Succeeded) return BadRequest("Unable to update password");
|
||||||
|
|
||||||
|
return Ok($"{resetPasswordDto.UserName}'s Password has been reset.");
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
|
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
|
||||||
{
|
{
|
||||||
@ -59,15 +77,14 @@ namespace API.Controllers
|
|||||||
if (registerDto.IsAdmin)
|
if (registerDto.IsAdmin)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"{user.UserName} is being registered as admin. Granting access to all libraries.");
|
_logger.LogInformation($"{user.UserName} is being registered as admin. Granting access to all libraries.");
|
||||||
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
|
||||||
foreach (var lib in libraries)
|
foreach (var lib in libraries)
|
||||||
{
|
{
|
||||||
lib.AppUsers ??= new List<AppUser>();
|
lib.AppUsers ??= new List<AppUser>();
|
||||||
lib.AppUsers.Add(user);
|
lib.AppUsers.Add(user);
|
||||||
}
|
}
|
||||||
|
if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogInformation("There was an issue granting library access. Please do this manually.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _unitOfWork.Complete()) _logger.LogInformation("There was an issue granting library access. Please do this manually.");
|
|
||||||
|
|
||||||
return new UserDto
|
return new UserDto
|
||||||
{
|
{
|
||||||
@ -80,7 +97,11 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
||||||
{
|
{
|
||||||
var user = await _userManager.Users
|
var user = await _userManager.Users
|
||||||
.SingleOrDefaultAsync(x => x.UserName == loginDto.Username.ToLower());
|
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
|
||||||
|
|
||||||
|
var debugUsers = await _userManager.Users.Select(x => x.NormalizedUserName).ToListAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation($"All Users: {String.Join(",", debugUsers)}");
|
||||||
|
|
||||||
if (user == null) return Unauthorized("Invalid username");
|
if (user == null) return Unauthorized("Invalid username");
|
||||||
|
|
||||||
|
@ -144,6 +144,14 @@ namespace API.Controllers
|
|||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost("scan")]
|
[HttpPost("scan")]
|
||||||
public ActionResult Scan(int libraryId)
|
public ActionResult Scan(int libraryId)
|
||||||
|
{
|
||||||
|
_taskScheduler.ScanLibrary(libraryId, false);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpPost("refresh-metadata")]
|
||||||
|
public ActionResult RefreshMetadata(int libraryId)
|
||||||
{
|
{
|
||||||
_taskScheduler.ScanLibrary(libraryId, true);
|
_taskScheduler.ScanLibrary(libraryId, true);
|
||||||
return Ok();
|
return Ok();
|
||||||
|
@ -60,6 +60,14 @@ namespace API.Controllers
|
|||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, user.Id));
|
return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, user.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpPost("scan")]
|
||||||
|
public ActionResult Scan(int libraryId, int seriesId)
|
||||||
|
{
|
||||||
|
_taskScheduler.ScanSeries(libraryId, seriesId);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("update-rating")]
|
[HttpPost("update-rating")]
|
||||||
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||||
|
66
API/Controllers/SettingsController.cs
Normal file
66
API/Controllers/SettingsController.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
|
using API.Interfaces;
|
||||||
|
using API.Services;
|
||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace API.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
public class SettingsController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly DataContext _dataContext;
|
||||||
|
private readonly ILogger<SettingsController> _logger;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly ITaskScheduler _taskScheduler;
|
||||||
|
|
||||||
|
public SettingsController(DataContext dataContext, ILogger<SettingsController> logger, IMapper mapper, ITaskScheduler taskScheduler)
|
||||||
|
{
|
||||||
|
_dataContext = dataContext;
|
||||||
|
_logger = logger;
|
||||||
|
_mapper = mapper;
|
||||||
|
_taskScheduler = taskScheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||||
|
{
|
||||||
|
var settings = await _dataContext.ServerSetting.Select(x => x).ToListAsync();
|
||||||
|
return _mapper.Map<ServerSettingDto>(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpPost("")]
|
||||||
|
public async Task<ActionResult> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"{User.GetUsername()} is updating Server Settings");
|
||||||
|
|
||||||
|
if (updateSettingsDto.CacheDirectory.Equals(string.Empty))
|
||||||
|
{
|
||||||
|
return BadRequest("Cache Directory cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(updateSettingsDto.CacheDirectory))
|
||||||
|
{
|
||||||
|
return BadRequest("Directory does not exist or is not accessible.");
|
||||||
|
}
|
||||||
|
// TODO: Figure out how to handle a change. This means that on clean, we need to clean up old cache
|
||||||
|
// directory and new one, but what if someone is reading?
|
||||||
|
// I can just clean both always, /cache/ is an owned folder, so users shouldn't use it.
|
||||||
|
|
||||||
|
|
||||||
|
//_dataContext.ServerSetting.Update
|
||||||
|
return BadRequest("Not Implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ namespace API.DTOs
|
|||||||
[Required]
|
[Required]
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(8, MinimumLength = 4)]
|
[StringLength(16, MinimumLength = 4)]
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
public bool IsAdmin { get; set; }
|
public bool IsAdmin { get; set; }
|
||||||
}
|
}
|
||||||
|
13
API/DTOs/ResetPasswordDto.cs
Normal file
13
API/DTOs/ResetPasswordDto.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class ResetPasswordDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string UserName { get; init; }
|
||||||
|
[Required]
|
||||||
|
[StringLength(16, MinimumLength = 4)]
|
||||||
|
public string Password { get; init; }
|
||||||
|
}
|
||||||
|
}
|
9
API/DTOs/ServerSettingDTO.cs
Normal file
9
API/DTOs/ServerSettingDTO.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class ServerSettingDto
|
||||||
|
{
|
||||||
|
public string CacheDirectory { get; set; }
|
||||||
|
// public string Kind { get; init; }
|
||||||
|
// public string Value { get; init; }
|
||||||
|
}
|
||||||
|
}
|
@ -24,11 +24,16 @@ namespace API.Data
|
|||||||
public DbSet<AppUser> AppUser { get; set; }
|
public DbSet<AppUser> AppUser { 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; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
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)
|
||||||
|
@ -53,15 +53,6 @@ namespace API.Data
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Library> GetLibraryForNameAsync(string libraryName)
|
|
||||||
{
|
|
||||||
return await _context.Library
|
|
||||||
.Where(x => x.Name == libraryName)
|
|
||||||
.Include(f => f.Folders)
|
|
||||||
.Include(s => s.Series)
|
|
||||||
.SingleAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteLibrary(int libraryId)
|
public async Task<bool> DeleteLibrary(int libraryId)
|
||||||
{
|
{
|
||||||
var library = await GetLibraryForIdAsync(libraryId);
|
var library = await GetLibraryForIdAsync(libraryId);
|
||||||
|
626
API/Data/Migrations/20210121180051_AddedServerSettings.Designer.cs
generated
Normal file
626
API/Data/Migrations/20210121180051_AddedServerSettings.Designer.cs
generated
Normal file
@ -0,0 +1,626 @@
|
|||||||
|
// <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("20210121180051_AddedServerSettings")]
|
||||||
|
partial class AddedServerSettings
|
||||||
|
{
|
||||||
|
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>("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.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Chapter")
|
||||||
|
.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("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(65535)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
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<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.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
.WithMany("Folders")
|
||||||
|
.HasForeignKey("LibraryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
.WithMany("Series")
|
||||||
|
.HasForeignKey("LibraryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithMany("Volumes")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AppUsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.Library", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("LibrariesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
API/Data/Migrations/20210121180051_AddedServerSettings.cs
Normal file
31
API/Data/Migrations/20210121180051_AddedServerSettings.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddedServerSettings : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ServerSetting",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Kind = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", maxLength: 65535, nullable: false),
|
||||||
|
RowVersion = table.Column<uint>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ServerSetting", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ServerSetting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
621
API/Data/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs
generated
Normal file
621
API/Data/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs
generated
Normal file
@ -0,0 +1,621 @@
|
|||||||
|
// <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("20210121215532_ServerSettingsAdjustment")]
|
||||||
|
partial class ServerSettingsAdjustment
|
||||||
|
{
|
||||||
|
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>("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.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Chapter")
|
||||||
|
.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("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CacheDirectory")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
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<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.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
.WithMany("Folders")
|
||||||
|
.HasForeignKey("LibraryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
.WithMany("Series")
|
||||||
|
.HasForeignKey("LibraryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithMany("Volumes")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AppUsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.Library", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("LibrariesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class ServerSettingsAdjustment : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Kind",
|
||||||
|
table: "ServerSetting");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Value",
|
||||||
|
table: "ServerSetting");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "CacheDirectory",
|
||||||
|
table: "ServerSetting",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CacheDirectory",
|
||||||
|
table: "ServerSetting");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Kind",
|
||||||
|
table: "ServerSetting",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Value",
|
||||||
|
table: "ServerSetting",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 65535,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
624
API/Data/Migrations/20210122165809_ServerSettingsChange.Designer.cs
generated
Normal file
624
API/Data/Migrations/20210122165809_ServerSettingsChange.Designer.cs
generated
Normal file
@ -0,0 +1,624 @@
|
|||||||
|
// <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("20210122165809_ServerSettingsChange")]
|
||||||
|
partial class ServerSettingsChange
|
||||||
|
{
|
||||||
|
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>("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.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Chapter")
|
||||||
|
.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("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
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<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.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
.WithMany("Folders")
|
||||||
|
.HasForeignKey("LibraryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
.WithMany("Series")
|
||||||
|
.HasForeignKey("LibraryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithMany("Volumes")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AppUsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.Library", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("LibrariesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
API/Data/Migrations/20210122165809_ServerSettingsChange.cs
Normal file
33
API/Data/Migrations/20210122165809_ServerSettingsChange.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class ServerSettingsChange : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "CacheDirectory",
|
||||||
|
table: "ServerSetting",
|
||||||
|
newName: "Value");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Key",
|
||||||
|
table: "ServerSetting",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Key",
|
||||||
|
table: "ServerSetting");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "Value",
|
||||||
|
table: "ServerSetting",
|
||||||
|
newName: "CacheDirectory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
620
API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs
generated
Normal file
620
API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs
generated
Normal file
@ -0,0 +1,620 @@
|
|||||||
|
// <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("20210122172455_ServerSettingsPrimaryKey")]
|
||||||
|
partial class ServerSettingsPrimaryKey
|
||||||
|
{
|
||||||
|
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>("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.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LibraryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
|
b.ToTable("FolderPath");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Chapter")
|
||||||
|
.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("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<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.FolderPath", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
.WithMany("Folders")
|
||||||
|
.HasForeignKey("LibraryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Volume", "Volume")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("VolumeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Volume");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Library", "Library")
|
||||||
|
.WithMany("Series")
|
||||||
|
.HasForeignKey("LibraryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Library");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithMany("Volumes")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AppUsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.Library", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("LibrariesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
|
b.Navigation("Ratings");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Folders");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Volumes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class ServerSettingsPrimaryKey : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_ServerSetting",
|
||||||
|
table: "ServerSetting");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Id",
|
||||||
|
table: "ServerSetting");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Key",
|
||||||
|
table: "ServerSetting",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_ServerSetting",
|
||||||
|
table: "ServerSetting",
|
||||||
|
column: "Key");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_ServerSetting",
|
||||||
|
table: "ServerSetting");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Key",
|
||||||
|
table: "ServerSetting",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Id",
|
||||||
|
table: "ServerSetting",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true);
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_ServerSetting",
|
||||||
|
table: "ServerSetting",
|
||||||
|
column: "Id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -296,6 +296,23 @@ namespace API.Data.Migrations
|
|||||||
b.ToTable("Series");
|
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 =>
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Services;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
namespace API.Data
|
namespace API.Data
|
||||||
@ -25,5 +28,23 @@ namespace API.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task SeedSettings(DataContext context)
|
||||||
|
{
|
||||||
|
// NOTE: This needs to check if settings already exists before inserting.
|
||||||
|
// IList<ServerSetting> defaultSettings = new List<ServerSetting>()
|
||||||
|
// {
|
||||||
|
// new ServerSetting() {Key = "CacheDirectory", Value = CacheService.CacheDirectory}
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// await context.ServerSetting.AddRangeAsync(defaultSettings);
|
||||||
|
// await context.SaveChangesAsync();
|
||||||
|
// await context.ServerSetting.AddAsync(new ServerSetting
|
||||||
|
// {
|
||||||
|
// CacheDirectory = CacheService.CacheDirectory
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -160,7 +160,15 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
|
return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Series> GetSeriesByIdAsync(int seriesId)
|
||||||
|
{
|
||||||
|
return await _context.Series
|
||||||
|
.Include(s => s.Volumes)
|
||||||
|
.Where(s => s.Id == seriesId)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -9,6 +9,8 @@ namespace API.Entities
|
|||||||
[Description("Comic")]
|
[Description("Comic")]
|
||||||
Comic = 1,
|
Comic = 1,
|
||||||
[Description("Book")]
|
[Description("Book")]
|
||||||
Book = 2
|
Book = 2,
|
||||||
|
[Description("Webtoon")]
|
||||||
|
Webtoon = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
20
API/Entities/ServerSetting.cs
Normal file
20
API/Entities/ServerSetting.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Entities.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Entities
|
||||||
|
{
|
||||||
|
public class ServerSetting : IHasConcurrencyToken
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
[ConcurrencyCheck]
|
||||||
|
public uint RowVersion { get; set; }
|
||||||
|
public void OnSavingChanges()
|
||||||
|
{
|
||||||
|
RowVersion++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using Hangfire.LiteDB;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Extensions
|
namespace API.Extensions
|
||||||
{
|
{
|
||||||
@ -21,6 +22,7 @@ namespace API.Extensions
|
|||||||
services.AddScoped<ITokenService, TokenService>();
|
services.AddScoped<ITokenService, TokenService>();
|
||||||
services.AddScoped<ICacheService, CacheService>();
|
services.AddScoped<ICacheService, CacheService>();
|
||||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
|
services.AddScoped<IScannerService, ScannerService>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +31,12 @@ namespace API.Extensions
|
|||||||
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddLogging(loggingBuilder =>
|
||||||
|
{
|
||||||
|
var loggingSection = config.GetSection("Logging");
|
||||||
|
loggingBuilder.AddFile(loggingSection);
|
||||||
|
});
|
||||||
|
|
||||||
services.AddHangfire(configuration => configuration
|
services.AddHangfire(configuration => configuration
|
||||||
.UseSimpleAssemblyNameTypeSerializer()
|
.UseSimpleAssemblyNameTypeSerializer()
|
||||||
.UseRecommendedSerializerSettings()
|
.UseRecommendedSerializerSettings()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace API.Extensions
|
namespace API.Extensions
|
||||||
@ -50,7 +51,6 @@ 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);
|
||||||
Console.WriteLine($"Renaming/Moving file to: {newPath}");
|
|
||||||
file.MoveTo(newPath);
|
file.MoveTo(newPath);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Helpers.Converters;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
|
||||||
namespace API.Helpers
|
namespace API.Helpers
|
||||||
@ -24,6 +26,9 @@ namespace API.Helpers
|
|||||||
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
|
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
|
||||||
|
|
||||||
CreateMap<RegisterDto, AppUser>();
|
CreateMap<RegisterDto, AppUser>();
|
||||||
|
|
||||||
|
CreateMap<IEnumerable<ServerSetting>, ServerSettingDto>()
|
||||||
|
.ConvertUsing<ServerSettingConverter>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
28
API/Helpers/Converters/ServerSettingConverter.cs
Normal file
28
API/Helpers/Converters/ServerSettingConverter.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
using AutoMapper;
|
||||||
|
|
||||||
|
namespace API.Helpers.Converters
|
||||||
|
{
|
||||||
|
public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>, ServerSettingDto>
|
||||||
|
{
|
||||||
|
public ServerSettingDto Convert(IEnumerable<ServerSetting> source, ServerSettingDto destination, ResolutionContext context)
|
||||||
|
{
|
||||||
|
destination = new ServerSettingDto();
|
||||||
|
foreach (var row in source)
|
||||||
|
{
|
||||||
|
switch (row.Key)
|
||||||
|
{
|
||||||
|
case "CacheDirectory":
|
||||||
|
destination.CacheDirectory = row.Value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,66 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
|
||||||
using API.Extensions;
|
|
||||||
using NetVips;
|
|
||||||
|
|
||||||
namespace API.IO
|
|
||||||
{
|
|
||||||
public static class ImageProvider
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Generates byte array of cover image.
|
|
||||||
/// Given a path to a compressed file (zip, rar, cbz, cbr, etc), will ensure the first image is returned unless
|
|
||||||
/// a folder.extension exists in the root directory of the compressed file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filepath"></param>
|
|
||||||
/// <param name="createThumbnail">Create a smaller variant of file extracted from archive. Archive images are usually 1MB each.</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static byte[] GetCoverImage(string filepath, bool createThumbnail = false)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(filepath) || !File.Exists(filepath) || !Parser.Parser.IsArchive(filepath)) return Array.Empty<byte>();
|
|
||||||
|
|
||||||
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 entry = archive.Entries.Where(x => Path.HasExtension(x.FullName)).OrderBy(x => x.FullName).ToList()[0];
|
|
||||||
|
|
||||||
if (folder != null)
|
|
||||||
{
|
|
||||||
entry = folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createThumbnail)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var stream = entry.Open();
|
|
||||||
var thumbnail = Image.ThumbnailStream(stream, 320);
|
|
||||||
Console.WriteLine(thumbnail.ToString());
|
|
||||||
return thumbnail.WriteToBuffer(".jpg");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine("There was a critical error and prevented thumbnail generation.");
|
|
||||||
Console.WriteLine(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExtractEntryToImage(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] ExtractEntryToImage(ZipArchiveEntry entry)
|
|
||||||
{
|
|
||||||
using var stream = entry.Open();
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
stream.CopyTo(ms);
|
|
||||||
var data = ms.ToArray();
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,35 +13,6 @@ namespace API.Interfaces
|
|||||||
/// <returns>List of folder names</returns>
|
/// <returns>List of folder names</returns>
|
||||||
IEnumerable<string> ListDirectory(string rootPath);
|
IEnumerable<string> ListDirectory(string rootPath);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite
|
|
||||||
/// cover images if forceUpdate is true.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="libraryId">Library to scan against</param>
|
|
||||||
/// <param name="forceUpdate">Force overwriting for cover images</param>
|
|
||||||
void ScanLibrary(int libraryId, bool forceUpdate);
|
|
||||||
|
|
||||||
void ScanLibraries();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the path a volume would be extracted to.
|
|
||||||
/// Deprecated.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="volumeId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
string GetExtractPath(int volumeId);
|
|
||||||
|
|
||||||
Task<ImageDto> ReadImageAsync(string imagePath);
|
Task<ImageDto> ReadImageAsync(string imagePath);
|
||||||
|
|
||||||
/// <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>
|
|
||||||
string ExtractArchive(string archivePath, string extractPath);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,7 +14,6 @@ namespace API.Interfaces
|
|||||||
Task<Library> GetLibraryForIdAsync(int libraryId);
|
Task<Library> GetLibraryForIdAsync(int libraryId);
|
||||||
Task<IEnumerable<LibraryDto>> GetLibraryDtosForUsernameAsync(string userName);
|
Task<IEnumerable<LibraryDto>> GetLibraryDtosForUsernameAsync(string userName);
|
||||||
Task<IEnumerable<Library>> GetLibrariesAsync();
|
Task<IEnumerable<Library>> GetLibrariesAsync();
|
||||||
Task<Library> GetLibraryForNameAsync(string libraryName);
|
|
||||||
Task<bool> DeleteLibrary(int libraryId);
|
Task<bool> DeleteLibrary(int libraryId);
|
||||||
}
|
}
|
||||||
}
|
}
|
25
API/Interfaces/IScannerService.cs
Normal file
25
API/Interfaces/IScannerService.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs;
|
||||||
|
|
||||||
|
namespace API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IScannerService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite
|
||||||
|
/// cover images if forceUpdate is true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryId">Library to scan against</param>
|
||||||
|
/// <param name="forceUpdate">Force overwriting for cover images</param>
|
||||||
|
void ScanLibrary(int libraryId, bool forceUpdate);
|
||||||
|
|
||||||
|
void ScanLibraries();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a forced scan of just a series folder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryId"></param>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
void ScanSeries(int libraryId, int seriesId);
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@ namespace API.Interfaces
|
|||||||
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,8 +2,8 @@
|
|||||||
{
|
{
|
||||||
public interface ITaskScheduler
|
public interface ITaskScheduler
|
||||||
{
|
{
|
||||||
public void ScanLibrary(int libraryId, bool forceUpdate = false);
|
void ScanLibrary(int libraryId, bool forceUpdate = false);
|
||||||
|
void CleanupVolumes(int[] volumeIds);
|
||||||
public void CleanupVolumes(int[] volumeIds);
|
void ScanSeries(int libraryId, int seriesId);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -25,7 +25,6 @@ namespace API.Middleware
|
|||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
_logger.LogError("The middleware called");
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _next(context); // downstream middlewares or http call
|
await _next(context); // downstream middlewares or http call
|
||||||
|
@ -7,67 +7,92 @@ namespace API.Parser
|
|||||||
{
|
{
|
||||||
public static class Parser
|
public static class Parser
|
||||||
{
|
{
|
||||||
public static readonly string MangaFileExtensions = @"\.cbz|\.cbr|\.png|\.jpeg|\.jpg|\.zip|\.rar";
|
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";
|
||||||
|
|
||||||
//?: 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[]
|
||||||
{
|
{
|
||||||
// Historys Strongest Disciple Kenichi_v11_c90-98.zip
|
// Dance in the Vampire Bund v16-17
|
||||||
new Regex(
|
new Regex(
|
||||||
|
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
|
||||||
@"(?<Series>.*)(\b|_)v(?<Volume>\d+)",
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d*)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
// Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
|
// Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(vol. ?)(?<Volume>0*[1-9]+)",
|
@"(vol\.? ?)(?<Volume>0*[1-9]+)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
// Dance in the Vampire Bund v16-17
|
// Tonikaku Cawaii [Volume 11].cbz
|
||||||
new Regex(
|
new Regex(
|
||||||
|
@"(volume )(?<Volume>0?[1-9]+)",
|
||||||
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)",
|
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
|
// Tower Of God S01 014 (CBT) (digital).cbz
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(?:v)(?<Volume>0*[1-9]+)",
|
@"(?<Series>.*)(\b|_|)(S(?<Volume>\d+))",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Regex[] MangaSeriesRegex = new[]
|
private static readonly Regex[] MangaSeriesRegex = new[]
|
||||||
{
|
{
|
||||||
|
// Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d*)( |_)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
// Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto]
|
// Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto]
|
||||||
new Regex(
|
new Regex(
|
||||||
|
|
||||||
@"(?<Series>.*)( - )(?:v|vo|c)\d",
|
@"(?<Series>.*)( - )(?:v|vo|c)\d",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
|
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
|
||||||
new Regex(
|
new Regex(
|
||||||
|
@"(?<Series>.*) (\b|_|-)v",
|
||||||
@"(?<Series>.*)(\b|_)v",
|
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
//Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz
|
||||||
// Black Bullet
|
|
||||||
new Regex(
|
new Regex(
|
||||||
|
@"(?<Series>.*)(?: _|-|\[|\() ?v",
|
||||||
@"(?<Series>.*)(\b|_)(v|vo|c)",
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
//Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans]
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*)(\bc\d+\b)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
//Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip must be before [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
|
||||||
|
// due to duplicate version identifiers in file.
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*)(v|s)\d+(-\d+)?(_| )",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
//[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*)(v|s)\d+(-\d+)?",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*) (?<Chapter>\d+) (?:\(\d{4}\)) ",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*)(?:, Chapter )(?<Chapter>\d+)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
// Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)
|
// Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)
|
||||||
new Regex(
|
new Regex(
|
||||||
|
|
||||||
@"(?<Series>.*)\(\d",
|
@"(?<Series>.*)\(\d",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
// [BAA]_Darker_than_Black_c1 (This is very greedy, make sure it's always last)
|
// Black Bullet (This is very loose, keep towards bottom) (?<Series>.*)(_)(v|vo|c|volume)
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(?<Series>.*)(\b|_)(c)",
|
@"(?<Series>.*)(_)(v|vo|c|volume)( |_)\d+",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
// Darker Than Black (This takes anything, we have to account for perfectly named folders)
|
// Akiiro Bousou Biyori - 01.jpg, Beelzebub_172_RHS.zip, Cynthia the Mission 29.rar
|
||||||
new Regex(
|
new Regex(
|
||||||
@"(?<Series>.*)",
|
@"^(?!Vol)(?<Series>.*)( |_)(\d+)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// [BAA]_Darker_than_Black_c1 (This is very greedy, make sure it's close to last)
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*)( |_)(c)\d+",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Regex[] ReleaseGroupRegex = new[]
|
private static readonly Regex[] ReleaseGroupRegex = new[]
|
||||||
@ -83,7 +108,6 @@ namespace API.Parser
|
|||||||
private static readonly Regex[] MangaChapterRegex = new[]
|
private static readonly Regex[] MangaChapterRegex = new[]
|
||||||
{
|
{
|
||||||
new Regex(
|
new Regex(
|
||||||
|
|
||||||
@"(c|ch)(\.? ?)(?<Chapter>\d+-?\d*)",
|
@"(c|ch)(\.? ?)(?<Chapter>\d+-?\d*)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
|
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
|
||||||
@ -91,28 +115,118 @@ namespace API.Parser
|
|||||||
|
|
||||||
@"v\d+\.(?<Chapter>\d+-?\d*)",
|
@"v\d+\.(?<Chapter>\d+-?\d*)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*) (?<Chapter>\d+) (?:\(\d{4}\))",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// Tower Of God S01 014 (CBT) (digital).cbz
|
||||||
|
new Regex(
|
||||||
|
@"(?<Series>.*) S(?<Volume>\d+) (?<Chapter>\d+)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// Beelzebub_01_[Noodles].zip
|
||||||
|
new Regex(
|
||||||
|
@"^((?!v|vo|vol|Volume).)*( |_)(?<Chapter>\.?\d+)( |_)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// Yumekui-Merry_DKThias_Chapter21.zip
|
||||||
|
new Regex(
|
||||||
|
@"Chapter(?<Chapter>\d+(-\d+)?)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
private static readonly Regex[] MangaEditionRegex = {
|
||||||
|
//Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
||||||
|
new Regex(
|
||||||
|
@"(?<Edition>({|\(|\[).* Edition(}|\)|\]))",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
//Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
||||||
|
new Regex(
|
||||||
|
@"(\b|_)(?<Edition>Omnibus)(\b|_)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Regex[] CleanupRegex =
|
||||||
|
{
|
||||||
|
// (), {}, []
|
||||||
|
new Regex(
|
||||||
|
@"(?<Cleanup>(\{\}|\[\]|\(\)))",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// (Complete)
|
||||||
|
new Regex(
|
||||||
|
@"(?<Cleanup>(\{Complete\}|\[Complete\]|\(Complete\)))",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
// Anything in parenthesis
|
||||||
|
new Regex(
|
||||||
|
@"\(.*\)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
public static ParserInfo Parse(string filePath)
|
/// <summary>
|
||||||
|
/// Parses information out of a file path. Will fallback to using directory name if Series couldn't be parsed
|
||||||
|
/// from filename.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath"></param>
|
||||||
|
/// <param name="rootPath">Root folder</param>
|
||||||
|
/// <returns><see cref="ParserInfo"/> or null if Series was empty</returns>
|
||||||
|
public static ParserInfo? Parse(string filePath, string rootPath)
|
||||||
{
|
{
|
||||||
return new ParserInfo()
|
var fileName = Path.GetFileName(filePath);
|
||||||
|
var directoryName = (new FileInfo(filePath)).Directory?.Name;
|
||||||
|
var rootName = (new DirectoryInfo(rootPath)).Name;
|
||||||
|
|
||||||
|
var ret = new ParserInfo()
|
||||||
{
|
{
|
||||||
Chapters = ParseChapter(filePath),
|
Chapters = ParseChapter(fileName),
|
||||||
Series = ParseSeries(filePath),
|
Series = ParseSeries(fileName),
|
||||||
Volumes = ParseVolume(filePath),
|
Volumes = ParseVolume(fileName),
|
||||||
Filename = filePath,
|
Filename = fileName,
|
||||||
Format = ParseFormat(filePath)
|
Format = ParseFormat(filePath),
|
||||||
|
FullFilePath = filePath
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ret.Series == string.Empty && directoryName != null && directoryName != rootName)
|
||||||
|
{
|
||||||
|
ret.Series = ParseSeries(directoryName);
|
||||||
|
if (ret.Series == string.Empty) ret.Series = CleanTitle(directoryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var edition = ParseEdition(fileName);
|
||||||
|
if (!string.IsNullOrEmpty(edition))
|
||||||
|
{
|
||||||
|
ret.Series = CleanTitle(ret.Series.Replace(edition, ""));
|
||||||
|
ret.Edition = edition;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return ret.Series == string.Empty ? null : ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MangaFormat ParseFormat(string filePath)
|
private static MangaFormat ParseFormat(string filePath)
|
||||||
{
|
{
|
||||||
if (IsArchive(filePath)) return MangaFormat.Archive;
|
if (IsArchive(filePath)) return MangaFormat.Archive;
|
||||||
if (IsImage(filePath)) return MangaFormat.Image;
|
if (IsImage(filePath)) return MangaFormat.Image;
|
||||||
return MangaFormat.Unknown;
|
return MangaFormat.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ParseEdition(string filePath)
|
||||||
|
{
|
||||||
|
foreach (var regex in MangaEditionRegex)
|
||||||
|
{
|
||||||
|
var matches = regex.Matches(filePath);
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
if (match.Groups["Edition"].Success && match.Groups["Edition"].Value != string.Empty)
|
||||||
|
{
|
||||||
|
var edition = match.Groups["Edition"].Value.Replace("{", "").Replace("}", "")
|
||||||
|
.Replace("[", "").Replace("]", "").Replace("(", "").Replace(")", "");
|
||||||
|
|
||||||
|
return edition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public static string ParseSeries(string filename)
|
public static string ParseSeries(string filename)
|
||||||
{
|
{
|
||||||
@ -121,16 +235,14 @@ namespace API.Parser
|
|||||||
var matches = regex.Matches(filename);
|
var matches = regex.Matches(filename);
|
||||||
foreach (Match match in matches)
|
foreach (Match match in matches)
|
||||||
{
|
{
|
||||||
if (match.Groups["Volume"] != Match.Empty)
|
if (match.Groups["Series"].Success && match.Groups["Series"].Value != string.Empty)
|
||||||
{
|
{
|
||||||
return CleanTitle(match.Groups["Series"].Value);
|
return CleanTitle(match.Groups["Series"].Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("Unable to parse {0}", filename);
|
return string.Empty;
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ParseVolume(string filename)
|
public static string ParseVolume(string filename)
|
||||||
@ -140,16 +252,19 @@ namespace API.Parser
|
|||||||
var matches = regex.Matches(filename);
|
var matches = regex.Matches(filename);
|
||||||
foreach (Match match in matches)
|
foreach (Match match in matches)
|
||||||
{
|
{
|
||||||
if (match.Groups["Volume"] != Match.Empty)
|
if (match.Groups["Volume"] == Match.Empty) continue;
|
||||||
{
|
|
||||||
return RemoveLeadingZeroes(match.Groups["Volume"].Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var value = match.Groups["Volume"].Value;
|
||||||
|
if (!value.Contains("-")) return RemoveLeadingZeroes(match.Groups["Volume"].Value);
|
||||||
|
var tokens = value.Split("-");
|
||||||
|
var from = RemoveLeadingZeroes(tokens[0]);
|
||||||
|
var to = RemoveLeadingZeroes(tokens[1]);
|
||||||
|
return $"{@from}-{to}";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("Unable to parse {0}", filename);
|
return "0";
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ParseChapter(string filename)
|
public static string ParseChapter(string filename)
|
||||||
@ -163,7 +278,6 @@ namespace API.Parser
|
|||||||
{
|
{
|
||||||
var value = match.Groups["Chapter"].Value;
|
var value = match.Groups["Chapter"].Value;
|
||||||
|
|
||||||
|
|
||||||
if (value.Contains("-"))
|
if (value.Contains("-"))
|
||||||
{
|
{
|
||||||
var tokens = value.Split("-");
|
var tokens = value.Split("-");
|
||||||
@ -180,6 +294,23 @@ namespace API.Parser
|
|||||||
|
|
||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string RemoveEditionTagHolders(string title)
|
||||||
|
{
|
||||||
|
foreach (var regex in CleanupRegex)
|
||||||
|
{
|
||||||
|
var matches = regex.Matches(title);
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
title = title.Replace(match.Value, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Translates _ -> spaces, trims front and back of string, removes release groups
|
/// Translates _ -> spaces, trims front and back of string, removes release groups
|
||||||
@ -187,6 +318,21 @@ namespace API.Parser
|
|||||||
/// <param name="title"></param>
|
/// <param name="title"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string CleanTitle(string title)
|
public static string CleanTitle(string title)
|
||||||
|
{
|
||||||
|
title = RemoveReleaseGroup(title);
|
||||||
|
|
||||||
|
title = RemoveEditionTagHolders(title);
|
||||||
|
|
||||||
|
title = title.Replace("_", " ").Trim();
|
||||||
|
if (title.EndsWith("-"))
|
||||||
|
{
|
||||||
|
title = title.Substring(0, title.Length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return title.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveReleaseGroup(string title)
|
||||||
{
|
{
|
||||||
foreach (var regex in ReleaseGroupRegex)
|
foreach (var regex in ReleaseGroupRegex)
|
||||||
{
|
{
|
||||||
@ -200,8 +346,7 @@ namespace API.Parser
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
title = title.Replace("_", " ");
|
return title;
|
||||||
return title.Trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -235,7 +380,8 @@ namespace API.Parser
|
|||||||
|
|
||||||
public static string RemoveLeadingZeroes(string title)
|
public static string RemoveLeadingZeroes(string title)
|
||||||
{
|
{
|
||||||
return title.TrimStart(new[] { '0' });
|
var ret = title.TrimStart(new[] { '0' });
|
||||||
|
return ret == string.Empty ? "0" : ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsArchive(string filePath)
|
public static bool IsArchive(string filePath)
|
||||||
|
@ -9,15 +9,21 @@ namespace API.Parser
|
|||||||
public class ParserInfo
|
public class ParserInfo
|
||||||
{
|
{
|
||||||
// This can be multiple
|
// This can be multiple
|
||||||
public string Chapters { get; set; }
|
public string Chapters { get; set; } = "";
|
||||||
public string Series { get; set; }
|
public string Series { get; set; } = "";
|
||||||
// This can be multiple
|
// This can be multiple
|
||||||
public string Volumes { get; set; }
|
public string Volumes { get; set; } = "";
|
||||||
public string Filename { get; init; }
|
public string Filename { get; init; } = "";
|
||||||
public string FullFilePath { get; set; }
|
public string FullFilePath { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raw (image), Archive
|
/// <see cref="MangaFormat"/> that represents the type of the file (so caching service knows how to cache for reading)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MangaFormat Format { get; set; }
|
public MangaFormat Format { get; set; } = MangaFormat.Unknown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This can potentially story things like "Omnibus, Color, Full Contact Edition, Extra, Final, etc"
|
||||||
|
/// </summary>
|
||||||
|
public string Edition { get; set; } = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -31,6 +31,7 @@ namespace API
|
|||||||
// Apply all migrations on startup
|
// Apply all migrations on startup
|
||||||
await context.Database.MigrateAsync();
|
await context.Database.MigrateAsync();
|
||||||
await Seed.SeedRoles(roleManager);
|
await Seed.SeedRoles(roleManager);
|
||||||
|
await Seed.SeedSettings(context);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -43,6 +44,11 @@ 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>();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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;
|
||||||
@ -16,7 +17,7 @@ namespace API.Services
|
|||||||
private readonly ILogger<CacheService> _logger;
|
private readonly ILogger<CacheService> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly NumericComparer _numericComparer;
|
private readonly NumericComparer _numericComparer;
|
||||||
private 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(IDirectoryService directoryService, ILogger<CacheService> logger, IUnitOfWork unitOfWork)
|
||||||
{
|
{
|
||||||
@ -28,7 +29,8 @@ namespace API.Services
|
|||||||
|
|
||||||
private bool CacheDirectoryIsAccessible()
|
private bool CacheDirectoryIsAccessible()
|
||||||
{
|
{
|
||||||
var di = new DirectoryInfo(_cacheDirectory);
|
_logger.LogDebug($"Checking if valid Cache directory: {CacheDirectory}");
|
||||||
|
var di = new DirectoryInfo(CacheDirectory);
|
||||||
return di.Exists;
|
return di.Exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +45,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
var extractPath = GetVolumeCachePath(volumeId, file);
|
var extractPath = GetVolumeCachePath(volumeId, file);
|
||||||
|
|
||||||
_directoryService.ExtractArchive(file.FilePath, extractPath);
|
ExtractArchive(file.FilePath, extractPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return volume;
|
return volume;
|
||||||
@ -55,11 +57,11 @@ namespace API.Services
|
|||||||
|
|
||||||
if (!CacheDirectoryIsAccessible())
|
if (!CacheDirectoryIsAccessible())
|
||||||
{
|
{
|
||||||
_logger.LogError($"Cache directory {_cacheDirectory} is not accessible or does not exist.");
|
_logger.LogError($"Cache directory {CacheDirectory} is not accessible or does not exist.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectoryInfo di = new DirectoryInfo(_cacheDirectory);
|
DirectoryInfo di = new DirectoryInfo(CacheDirectory);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -79,7 +81,7 @@ namespace API.Services
|
|||||||
|
|
||||||
foreach (var volume in volumeIds)
|
foreach (var volume in volumeIds)
|
||||||
{
|
{
|
||||||
var di = new DirectoryInfo(Path.Join(_cacheDirectory, volume + ""));
|
var di = new DirectoryInfo(Path.Join(CacheDirectory, volume + ""));
|
||||||
if (di.Exists)
|
if (di.Exists)
|
||||||
{
|
{
|
||||||
di.Delete(true);
|
di.Delete(true);
|
||||||
@ -88,6 +90,45 @@ namespace API.Services
|
|||||||
}
|
}
|
||||||
_logger.LogInformation("Cache directory purged");
|
_logger.LogInformation("Cache directory purged");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts an archive to a temp cache directory. Returns path to new directory. If temp cache directory already exists,
|
||||||
|
/// will return that without performing an extraction. Returns empty string if there are any invalidations which would
|
||||||
|
/// prevent operations to perform correctly (missing archivePath file, empty archive, etc).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="archivePath">A valid file to an archive file.</param>
|
||||||
|
/// <param name="extractPath">Path to extract to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string ExtractArchive(string archivePath, string extractPath)
|
||||||
|
{
|
||||||
|
// NOTE: This is used by Cache Service
|
||||||
|
if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath))
|
||||||
|
{
|
||||||
|
_logger.LogError($"Archive {archivePath} could not be found.");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(extractPath))
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Archive {archivePath} has already been extracted. Returning existing folder.");
|
||||||
|
return extractPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
||||||
|
// TODO: Throw error if we couldn't extract
|
||||||
|
var needsFlattening = archive.Entries.Count > 0 && !Path.HasExtension(archive.Entries.ElementAt(0).FullName);
|
||||||
|
if (!archive.HasFiles() && !needsFlattening) return "";
|
||||||
|
|
||||||
|
archive.ExtractToDirectory(extractPath);
|
||||||
|
_logger.LogDebug($"Extracting archive to {extractPath}");
|
||||||
|
|
||||||
|
if (!needsFlattening) return extractPath;
|
||||||
|
|
||||||
|
_logger.LogInformation("Extracted archive is nested in root folder, flattening...");
|
||||||
|
new DirectoryInfo(extractPath).Flatten();
|
||||||
|
|
||||||
|
return extractPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private string GetVolumeCachePath(int volumeId, MangaFile file)
|
private string GetVolumeCachePath(int volumeId, MangaFile file)
|
||||||
|
@ -1,38 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
|
||||||
using API.Extensions;
|
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.IO;
|
|
||||||
using API.Parser;
|
|
||||||
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;
|
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
|
||||||
|
|
||||||
private ConcurrentDictionary<string, ConcurrentBag<ParserInfo>> _scannedSeries;
|
|
||||||
|
|
||||||
public DirectoryService(ILogger<DirectoryService> logger, IUnitOfWork unitOfWork)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_unitOfWork = unitOfWork;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -69,302 +51,23 @@ namespace API.Services
|
|||||||
|
|
||||||
return dirs;
|
return dirs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes files found during a library scan. Generates a collection of series->volume->files for DB processing later.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path of a file</param>
|
|
||||||
private void Process(string path)
|
|
||||||
{
|
|
||||||
var fileName = Path.GetFileName(path);
|
|
||||||
_logger.LogDebug($"Parsing file {fileName}");
|
|
||||||
|
|
||||||
var info = Parser.Parser.Parse(fileName);
|
|
||||||
info.FullFilePath = path;
|
|
||||||
if (info.Volumes == string.Empty)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConcurrentBag<ParserInfo> newBag = new ConcurrentBag<ParserInfo>();
|
|
||||||
if (_scannedSeries.TryGetValue(info.Series, out var tempBag))
|
|
||||||
{
|
|
||||||
var existingInfos = tempBag.ToArray();
|
|
||||||
foreach (var existingInfo in existingInfos)
|
|
||||||
{
|
|
||||||
newBag.Add(existingInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tempBag = new ConcurrentBag<ParserInfo>();
|
|
||||||
}
|
|
||||||
|
|
||||||
newBag.Add(info);
|
|
||||||
|
|
||||||
if (!_scannedSeries.TryUpdate(info.Series, newBag, tempBag))
|
|
||||||
{
|
|
||||||
_scannedSeries.TryAdd(info.Series, newBag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Series UpdateSeries(Series series, ParserInfo[] infos, bool forceUpdate)
|
public async Task<ImageDto> ReadImageAsync(string imagePath)
|
||||||
{
|
{
|
||||||
var volumes = UpdateVolumes(series, infos, forceUpdate);
|
using var image = Image.NewFromFile(imagePath);
|
||||||
series.Volumes = volumes;
|
|
||||||
series.Pages = volumes.Sum(v => v.Pages);
|
|
||||||
if (series.CoverImage == null || forceUpdate)
|
|
||||||
{
|
|
||||||
series.CoverImage = volumes.OrderBy(x => x.Number).FirstOrDefault()?.CoverImage;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrEmpty(series.Summary) || forceUpdate)
|
|
||||||
{
|
|
||||||
series.Summary = ""; // TODO: Check if comicInfo.xml in file and parse metadata out.
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return series;
|
return new ImageDto
|
||||||
}
|
|
||||||
|
|
||||||
private MangaFile CreateMangaFile(ParserInfo info)
|
|
||||||
{
|
|
||||||
_logger.LogDebug($"Creating File Entry for {info.FullFilePath}");
|
|
||||||
int.TryParse(info.Chapters, out var chapter);
|
|
||||||
_logger.LogDebug($"Found Chapter: {chapter}");
|
|
||||||
return new MangaFile()
|
|
||||||
{
|
{
|
||||||
FilePath = info.FullFilePath,
|
Content = await File.ReadAllBytesAsync(imagePath),
|
||||||
Chapter = chapter,
|
Filename = Path.GetFileNameWithoutExtension(imagePath),
|
||||||
Format = info.Format,
|
FullPath = Path.GetFullPath(imagePath),
|
||||||
NumberOfPages = GetNumberOfPagesFromArchive(info.FullFilePath)
|
Width = image.Width,
|
||||||
|
Height = image.Height,
|
||||||
|
Format = image.Format
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates or Updates volumes for a given series
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="series">Series wanting to be updated</param>
|
|
||||||
/// <param name="infos">Parser info</param>
|
|
||||||
/// <param name="forceUpdate">Forces metadata update (cover image) even if it's already been set.</param>
|
|
||||||
/// <returns>Updated Volumes for given series</returns>
|
|
||||||
private ICollection<Volume> UpdateVolumes(Series series, ParserInfo[] infos, bool forceUpdate)
|
|
||||||
{
|
|
||||||
ICollection<Volume> volumes = new List<Volume>();
|
|
||||||
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
|
|
||||||
|
|
||||||
foreach (var info in infos)
|
|
||||||
{
|
|
||||||
var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
|
|
||||||
if (existingVolume != null)
|
|
||||||
{
|
|
||||||
var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
|
||||||
if (existingFile != null)
|
|
||||||
{
|
|
||||||
existingFile.Chapter = Int32.Parse(info.Chapters);
|
|
||||||
existingFile.Format = info.Format;
|
|
||||||
existingFile.NumberOfPages = GetNumberOfPagesFromArchive(info.FullFilePath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existingVolume.Files.Add(CreateMangaFile(info));
|
|
||||||
}
|
|
||||||
|
|
||||||
volumes.Add(existingVolume);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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 = Int32.Parse(info.Volumes),
|
|
||||||
Files = new List<MangaFile>()
|
|
||||||
{
|
|
||||||
CreateMangaFile(info)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
volumes.Add(vol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"Adding volume {volumes.Last().Number} with File: {info.Filename}");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var volume in volumes)
|
|
||||||
{
|
|
||||||
if (forceUpdate || volume.CoverImage == null || !volume.Files.Any())
|
|
||||||
{
|
|
||||||
var firstFile = volume.Files.OrderBy(x => x.Chapter).FirstOrDefault()?.FilePath;
|
|
||||||
volume.CoverImage = ImageProvider.GetCoverImage(firstFile, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
volume.Pages = volume.Files.Sum(x => x.NumberOfPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
return volumes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ScanLibraries()
|
|
||||||
{
|
|
||||||
var libraries = Task.Run(() => _unitOfWork.LibraryRepository.GetLibrariesAsync()).Result.ToList();
|
|
||||||
foreach (var lib in libraries)
|
|
||||||
{
|
|
||||||
ScanLibrary(lib.Id, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ScanLibrary(int libraryId, bool forceUpdate)
|
|
||||||
{
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
Library library;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
library = Task.Run(() => _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId)).Result;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// This usually only fails if user is not authenticated.
|
|
||||||
_logger.LogError($"There was an issue fetching Library {libraryId}.", ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_scannedSeries = new ConcurrentDictionary<string, ConcurrentBag<ParserInfo>>();
|
|
||||||
_logger.LogInformation($"Beginning scan on {library.Name}");
|
|
||||||
|
|
||||||
var totalFiles = 0;
|
|
||||||
foreach (var folderPath in library.Folders)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
totalFiles = TraverseTreeParallelForEach(folderPath.Path, (f) =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Process(f);
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException exception)
|
|
||||||
{
|
|
||||||
_logger.LogError(exception, "The file could not be found");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex) {
|
|
||||||
_logger.LogError(ex, $"The directory '{folderPath}' does not exist");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var filtered = _scannedSeries.Where(kvp => !kvp.Value.IsEmpty);
|
|
||||||
var series = filtered.ToImmutableDictionary(v => v.Key, v => v.Value);
|
|
||||||
|
|
||||||
// Perform DB activities
|
|
||||||
var allSeries = Task.Run(() => _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId)).Result.ToList();
|
|
||||||
foreach (var seriesKey in series.Keys)
|
|
||||||
{
|
|
||||||
var mangaSeries = allSeries.SingleOrDefault(s => s.Name == seriesKey) ?? new Series
|
|
||||||
{
|
|
||||||
Name = seriesKey,
|
|
||||||
OriginalName = seriesKey,
|
|
||||||
SortName = seriesKey,
|
|
||||||
Summary = ""
|
|
||||||
};
|
|
||||||
mangaSeries = UpdateSeries(mangaSeries, series[seriesKey].ToArray(), forceUpdate);
|
|
||||||
_logger.LogInformation($"Created/Updated series {mangaSeries.Name} for {library.Name} library");
|
|
||||||
library.Series ??= new List<Series>();
|
|
||||||
library.Series.Add(mangaSeries);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove series that are no longer on disk
|
|
||||||
foreach (var existingSeries in allSeries)
|
|
||||||
{
|
|
||||||
if (!series.ContainsKey(existingSeries.Name) || !series.ContainsKey(existingSeries.OriginalName))
|
|
||||||
{
|
|
||||||
// Delete series, there is no file to backup any longer.
|
|
||||||
library.Series.Remove(existingSeries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_unitOfWork.LibraryRepository.Update(library);
|
|
||||||
|
|
||||||
if (Task.Run(() => _unitOfWork.Complete()).Result)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogError("There was a critical error that resulted in a failed scan. Please rescan.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_scannedSeries = null;
|
|
||||||
_logger.LogInformation("Processed {0} files in {1} milliseconds for {2}", totalFiles, sw.ElapsedMilliseconds, library.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetExtractPath(int volumeId)
|
|
||||||
{
|
|
||||||
return Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/");
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ExtractArchive(string archivePath, string extractPath)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetNumberOfPagesFromArchive(string archivePath)
|
|
||||||
{
|
|
||||||
if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath))
|
|
||||||
{
|
|
||||||
_logger.LogError($"Archive {archivePath} could not be found.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
|
||||||
return archive.Entries.Count(e => Parser.Parser.IsImage(e.FullName));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<ImageDto> ReadImageAsync(string imagePath)
|
|
||||||
{
|
|
||||||
using var image = Image.NewFromFile(imagePath);
|
|
||||||
|
|
||||||
return new ImageDto
|
|
||||||
{
|
|
||||||
Content = await File.ReadAllBytesAsync(imagePath),
|
|
||||||
Filename = Path.GetFileNameWithoutExtension(imagePath),
|
|
||||||
FullPath = Path.GetFullPath(imagePath),
|
|
||||||
Width = image.Width,
|
|
||||||
Height = image.Height,
|
|
||||||
Format = image.Format
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed
|
/// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed
|
||||||
@ -373,16 +76,16 @@ namespace API.Services
|
|||||||
/// <param name="root">Directory to scan</param>
|
/// <param name="root">Directory to scan</param>
|
||||||
/// <param name="action">Action to apply on file path</param>
|
/// <param name="action">Action to apply on file path</param>
|
||||||
/// <exception cref="ArgumentException"></exception>
|
/// <exception cref="ArgumentException"></exception>
|
||||||
private static int TraverseTreeParallelForEach(string root, Action<string> action)
|
public static int TraverseTreeParallelForEach(string root, Action<string> action)
|
||||||
{
|
{
|
||||||
//Count of files traversed and timer for diagnostic output
|
//Count of files traversed and timer for diagnostic output
|
||||||
int fileCount = 0;
|
var fileCount = 0;
|
||||||
|
|
||||||
// Determine whether to parallelize file processing on each folder based on processor count.
|
// Determine whether to parallelize file processing on each folder based on processor count.
|
||||||
int procCount = Environment.ProcessorCount;
|
var procCount = Environment.ProcessorCount;
|
||||||
|
|
||||||
// Data structure to hold names of subfolders to be examined for files.
|
// Data structure to hold names of subfolders to be examined for files.
|
||||||
Stack<string> dirs = new Stack<string>();
|
var dirs = new Stack<string>();
|
||||||
|
|
||||||
if (!Directory.Exists(root)) {
|
if (!Directory.Exists(root)) {
|
||||||
throw new ArgumentException("The directory doesn't exist");
|
throw new ArgumentException("The directory doesn't exist");
|
||||||
@ -390,7 +93,7 @@ namespace API.Services
|
|||||||
dirs.Push(root);
|
dirs.Push(root);
|
||||||
|
|
||||||
while (dirs.Count > 0) {
|
while (dirs.Count > 0) {
|
||||||
string currentDir = dirs.Pop();
|
var currentDir = dirs.Pop();
|
||||||
string[] subDirs;
|
string[] subDirs;
|
||||||
string[] files;
|
string[] files;
|
||||||
|
|
||||||
@ -409,7 +112,9 @@ namespace API.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
files = DirectoryService.GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions)
|
// TODO: In future, we need to take LibraryType into consideration for what extensions to allow (RAW should allow images)
|
||||||
|
// or we need to move this filtering to another area (Process)
|
||||||
|
files = GetFilesWithCertainExtensions(currentDir, Parser.Parser.MangaFileExtensions)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException e) {
|
catch (UnauthorizedAccessException e) {
|
||||||
|
402
API/Services/ScannerService.cs
Normal file
402
API/Services/ScannerService.cs
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
|
using API.Interfaces;
|
||||||
|
using API.Parser;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NetVips;
|
||||||
|
|
||||||
|
namespace API.Services
|
||||||
|
{
|
||||||
|
public class ScannerService : IScannerService
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly ILogger<ScannerService> _logger;
|
||||||
|
private ConcurrentDictionary<string, List<ParserInfo>> _scannedSeries;
|
||||||
|
|
||||||
|
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ScanLibraries()
|
||||||
|
{
|
||||||
|
var libraries = Task.Run(() => _unitOfWork.LibraryRepository.GetLibrariesAsync()).Result.ToList();
|
||||||
|
foreach (var lib in libraries)
|
||||||
|
{
|
||||||
|
ScanLibrary(lib.Id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ScanLibrary(int libraryId, bool forceUpdate)
|
||||||
|
{
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
Library library;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
library = Task.Run(() => _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId)).Result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// This usually only fails if user is not authenticated.
|
||||||
|
_logger.LogError($"There was an issue fetching Library {libraryId}.", ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scannedSeries = new ConcurrentDictionary<string, List<ParserInfo>>();
|
||||||
|
_logger.LogInformation($"Beginning scan on {library.Name}. Forcing metadata update: {forceUpdate}");
|
||||||
|
|
||||||
|
var totalFiles = 0;
|
||||||
|
foreach (var folderPath in library.Folders)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
totalFiles += DirectoryService.TraverseTreeParallelForEach(folderPath.Path, (f) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessFile(f, folderPath.Path);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException exception)
|
||||||
|
{
|
||||||
|
_logger.LogError(exception, "The file could not be found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex) {
|
||||||
|
_logger.LogError(ex, $"The directory '{folderPath}' does not exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered = _scannedSeries.Where(kvp => kvp.Value.Count != 0);
|
||||||
|
var series = filtered.ToImmutableDictionary(v => v.Key, v => v.Value);
|
||||||
|
|
||||||
|
// Perform DB activities
|
||||||
|
var allSeries = UpsertSeries(libraryId, forceUpdate, series, library);
|
||||||
|
|
||||||
|
// Remove series that are no longer on disk
|
||||||
|
RemoveSeriesNotOnDisk(allSeries, series, library);
|
||||||
|
|
||||||
|
_unitOfWork.LibraryRepository.Update(library);
|
||||||
|
|
||||||
|
if (Task.Run(() => _unitOfWork.Complete()).Result)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("There was a critical error that resulted in a failed scan. Please rescan.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_scannedSeries = null;
|
||||||
|
_logger.LogInformation("Processed {0} files in {1} milliseconds for {2}", totalFiles, sw.ElapsedMilliseconds, library.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Series> UpsertSeries(int libraryId, bool forceUpdate, ImmutableDictionary<string, List<ParserInfo>> series, Library library)
|
||||||
|
{
|
||||||
|
var allSeries = Task.Run(() => _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId)).Result.ToList();
|
||||||
|
foreach (var seriesKey in series.Keys)
|
||||||
|
{
|
||||||
|
var mangaSeries = allSeries.SingleOrDefault(s => s.Name == seriesKey) ?? new Series
|
||||||
|
{
|
||||||
|
Name = seriesKey,
|
||||||
|
OriginalName = seriesKey,
|
||||||
|
SortName = seriesKey,
|
||||||
|
Summary = ""
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mangaSeries = UpdateSeries(mangaSeries, series[seriesKey].ToArray(), forceUpdate);
|
||||||
|
_logger.LogInformation($"Created/Updated series {mangaSeries.Name} for {library.Name} library");
|
||||||
|
library.Series ??= new List<Series>();
|
||||||
|
library.Series.Add(mangaSeries);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"There was an error during scanning of library. {seriesKey} will be skipped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSeriesNotOnDisk(List<Series> allSeries, ImmutableDictionary<string, List<ParserInfo>> series, Library library)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
foreach (var existingSeries in allSeries)
|
||||||
|
{
|
||||||
|
if (!series.ContainsKey(existingSeries.Name) || !series.ContainsKey(existingSeries.OriginalName))
|
||||||
|
{
|
||||||
|
// Delete series, there is no file to backup any longer.
|
||||||
|
library.Series?.Remove(existingSeries);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_logger.LogInformation($"Removed {count} series that are no longer on disk");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to either add a new instance of a show mapping to the scannedSeries bag or adds to an existing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info"></param>
|
||||||
|
private void TrackSeries(ParserInfo info)
|
||||||
|
{
|
||||||
|
if (info.Series == string.Empty) return;
|
||||||
|
|
||||||
|
_scannedSeries.AddOrUpdate(info.Series, new List<ParserInfo>() {info}, (key, oldValue) =>
|
||||||
|
{
|
||||||
|
oldValue ??= new List<ParserInfo>();
|
||||||
|
if (!oldValue.Contains(info))
|
||||||
|
{
|
||||||
|
oldValue.Add(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes files found during a library scan.
|
||||||
|
/// Populates a collection of <see cref="ParserInfo"/> for DB updates later.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path of a file</param>
|
||||||
|
/// <param name="rootPath"></param>
|
||||||
|
private void ProcessFile(string path, string rootPath)
|
||||||
|
{
|
||||||
|
var info = Parser.Parser.Parse(path, rootPath);
|
||||||
|
|
||||||
|
if (info == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Could not parse series from {path}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSeries(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Series UpdateSeries(Series series, ParserInfo[] infos, bool forceUpdate)
|
||||||
|
{
|
||||||
|
var volumes = UpdateVolumes(series, infos, forceUpdate);
|
||||||
|
series.Volumes = volumes;
|
||||||
|
series.Pages = volumes.Sum(v => v.Pages);
|
||||||
|
if (series.CoverImage == null || forceUpdate)
|
||||||
|
{
|
||||||
|
var firstCover = volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0);
|
||||||
|
if (firstCover == null && volumes.Any())
|
||||||
|
{
|
||||||
|
firstCover = volumes.FirstOrDefault(x => x.Number == 0);
|
||||||
|
}
|
||||||
|
series.CoverImage = firstCover?.CoverImage;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(series.Summary) || forceUpdate)
|
||||||
|
{
|
||||||
|
series.Summary = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MangaFile CreateMangaFile(ParserInfo info)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Creating File Entry for {info.FullFilePath}");
|
||||||
|
|
||||||
|
int.TryParse(info.Chapters, out var chapter);
|
||||||
|
_logger.LogDebug($"Found Chapter: {chapter}");
|
||||||
|
return new MangaFile()
|
||||||
|
{
|
||||||
|
FilePath = info.FullFilePath,
|
||||||
|
Chapter = chapter,
|
||||||
|
Format = info.Format,
|
||||||
|
NumberOfPages = info.Format == MangaFormat.Archive ? GetNumberOfPagesFromArchive(info.FullFilePath): 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int MinimumNumberFromRange(string range)
|
||||||
|
{
|
||||||
|
var tokens = range.Split("-");
|
||||||
|
return Int32.Parse(tokens.Length >= 1 ? tokens[0] : range);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates or Updates volumes for a given series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="series">Series wanting to be updated</param>
|
||||||
|
/// <param name="infos">Parser info</param>
|
||||||
|
/// <param name="forceUpdate">Forces metadata update (cover image) even if it's already been set.</param>
|
||||||
|
/// <returns>Updated Volumes for given series</returns>
|
||||||
|
private ICollection<Volume> UpdateVolumes(Series series, ParserInfo[] infos, bool forceUpdate)
|
||||||
|
{
|
||||||
|
ICollection<Volume> volumes = new List<Volume>();
|
||||||
|
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
|
||||||
|
|
||||||
|
foreach (var info in infos)
|
||||||
|
{
|
||||||
|
var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
|
||||||
|
if (existingVolume != null)
|
||||||
|
{
|
||||||
|
var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
||||||
|
if (existingFile != null)
|
||||||
|
{
|
||||||
|
existingFile.Chapter = MinimumNumberFromRange(info.Chapters);
|
||||||
|
existingFile.Format = info.Format;
|
||||||
|
existingFile.NumberOfPages = GetNumberOfPagesFromArchive(info.FullFilePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (info.Format == MangaFormat.Archive)
|
||||||
|
{
|
||||||
|
existingVolume.Files.Add(CreateMangaFile(info));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Ignoring {info.Filename} as it is not an archive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes.Add(existingVolume);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create New Volume
|
||||||
|
existingVolume = volumes.SingleOrDefault(v => v.Name == info.Volumes);
|
||||||
|
if (existingVolume != null)
|
||||||
|
{
|
||||||
|
existingVolume.Files.Add(CreateMangaFile(info));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var vol = new Volume()
|
||||||
|
{
|
||||||
|
Name = info.Volumes,
|
||||||
|
Number = MinimumNumberFromRange(info.Volumes),
|
||||||
|
Files = new List<MangaFile>()
|
||||||
|
{
|
||||||
|
CreateMangaFile(info)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
volumes.Add(vol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation($"Adding volume {volumes.Last().Number} with File: {info.Filename}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var volume in volumes)
|
||||||
|
{
|
||||||
|
if (forceUpdate || volume.CoverImage == null || !volume.Files.Any())
|
||||||
|
{
|
||||||
|
var firstFile = volume.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||||
|
if (firstFile != null) volume.CoverImage = GetCoverImage(firstFile.FilePath, true); // ZIPFILE
|
||||||
|
}
|
||||||
|
|
||||||
|
volume.Pages = volume.Files.Sum(x => x.NumberOfPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void ScanSeries(int libraryId, int seriesId)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetNumberOfPagesFromArchive(string archivePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath))
|
||||||
|
{
|
||||||
|
_logger.LogError($"Archive {archivePath} could not be found.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug($"Getting Page numbers from {archivePath}");
|
||||||
|
|
||||||
|
using ZipArchive archive = ZipFile.OpenRead(archivePath); // ZIPFILE
|
||||||
|
return archive.Entries.Count(e => Parser.Parser.IsImage(e.FullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates byte array of cover image.
|
||||||
|
/// Given a path to a compressed file (zip, rar, cbz, cbr, etc), will ensure the first image is returned unless
|
||||||
|
/// a folder.extension exists in the root directory of the compressed file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filepath"></param>
|
||||||
|
/// <param name="createThumbnail">Create a smaller variant of file extracted from archive. Archive images are usually 1MB each.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public byte[] GetCoverImage(string filepath, bool createThumbnail = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(filepath) || !File.Exists(filepath) || !Parser.Parser.IsArchive(filepath)) return Array.Empty<byte>();
|
||||||
|
|
||||||
|
_logger.LogDebug($"Extracting Cover image from {filepath}");
|
||||||
|
using ZipArchive archive = ZipFile.OpenRead(filepath);
|
||||||
|
if (!archive.HasFiles()) return Array.Empty<byte>();
|
||||||
|
|
||||||
|
var folder = archive.Entries.SingleOrDefault(x => Path.GetFileNameWithoutExtension(x.Name).ToLower() == "folder");
|
||||||
|
var entries = archive.Entries.Where(x => Path.HasExtension(x.FullName) && Parser.Parser.IsImage(x.FullName)).OrderBy(x => x.FullName).ToList();
|
||||||
|
ZipArchiveEntry entry;
|
||||||
|
|
||||||
|
if (folder != null)
|
||||||
|
{
|
||||||
|
entry = folder;
|
||||||
|
} else if (!entries.Any())
|
||||||
|
{
|
||||||
|
return Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entry = entries[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (createThumbnail)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = entry.Open();
|
||||||
|
var thumbnail = Image.ThumbnailStream(stream, 320);
|
||||||
|
return thumbnail.WriteToBuffer(".jpg");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was a critical error and prevented thumbnail generation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtractEntryToImage(entry);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an exception when reading archive stream.");
|
||||||
|
return Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ExtractEntryToImage(ZipArchiveEntry entry)
|
||||||
|
{
|
||||||
|
using var stream = entry.Open();
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
var data = ms.ToArray();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -8,25 +8,30 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly ILogger<TaskScheduler> _logger;
|
private readonly ILogger<TaskScheduler> _logger;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IScannerService _scannerService;
|
||||||
public BackgroundJobServer Client => new BackgroundJobServer();
|
public BackgroundJobServer Client => new BackgroundJobServer();
|
||||||
|
|
||||||
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger,
|
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService)
|
||||||
IDirectoryService directoryService)
|
|
||||||
{
|
{
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_directoryService = directoryService;
|
_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);
|
RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily);
|
||||||
RecurringJob.AddOrUpdate(() => directoryService.ScanLibraries(), Cron.Daily);
|
RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), Cron.Daily);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ScanSeries(int libraryId, int seriesId)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Enqueuing series scan for series: {seriesId}");
|
||||||
|
BackgroundJob.Enqueue(() => _scannerService.ScanSeries(libraryId, seriesId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ScanLibrary(int libraryId, bool forceUpdate = false)
|
public void ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"Enqueuing library scan for: {libraryId}");
|
_logger.LogInformation($"Enqueuing library scan for: {libraryId}");
|
||||||
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(libraryId, forceUpdate));
|
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CleanupVolumes(int[] volumeIds)
|
public void CleanupVolumes(int[] volumeIds)
|
||||||
@ -34,5 +39,6 @@ namespace API.Services
|
|||||||
BackgroundJob.Enqueue(() => _cacheService.CleanupVolumes(volumeIds));
|
BackgroundJob.Enqueue(() => _cacheService.CleanupVolumes(volumeIds));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -42,18 +42,16 @@ namespace API
|
|||||||
{
|
{
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
|
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
|
||||||
|
app.UseHangfireDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app.UseHangfireDashboard();
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
//app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
// Ordering is important. Cors, authentication, authorization
|
// Ordering is important. Cors, authentication, authorization
|
||||||
app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("https://localhost:4200"));
|
app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200"));
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
@ -9,6 +9,12 @@
|
|||||||
"Microsoft": "Information",
|
"Microsoft": "Information",
|
||||||
"Microsoft.Hosting.Lifetime": "Information",
|
"Microsoft.Hosting.Lifetime": "Information",
|
||||||
"Hangfire": "Information"
|
"Hangfire": "Information"
|
||||||
|
},
|
||||||
|
"File": {
|
||||||
|
"Path": "kavita.log",
|
||||||
|
"Append": "True",
|
||||||
|
"FileSizeLimitBytes": 0,
|
||||||
|
"MaxRollingFiles": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user