From a01613f80ff3281bea9ca2a7afffb11a50310a0a Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 28 Apr 2021 16:16:22 -0500 Subject: [PATCH] EPUB Support (#178) * Added book filetype detection and reorganized tests due to size of file * Added ability to get basic Parse Info from Book and Pages. * We can now scan books and get them in a library with cover images. * Take the first image in the epub if the cover isn't set. * Implemented the ability to unzip the ebup to cache. Implemented a test api to load html files. * Just some test code to figure out how to approach this. * Fixed some merge conflicts * Removed some dead code from merge * Snapshot: I can now load everything properly into the UI by rewriting the urls before I send them back. I don't notice any lag from this method. It can be optimized further. * Implemented a way to load the content in the browser not via an iframe. * Added a note * Anchor mappings is complete. New anchors are updated so references now resolve to javascript:void() for UI to take care of internally loading and the appropriate page is mapped to it. Anchors that are external have target="_blank" added so they don't force you out of the app and styles are of course inlined. * Oops i need this * Table of contents api implemented (rough) and some small enhancements to codebase for books. * GetBookPageResources now only loads files from within the book. Nested chapter list support and images now use html parsing instead of string parsing. * Fonts now are remapped to load from endpoint. * book-resources now uses a key, ensuring the file is in proper format for lookup. Changed chapter list based on structure with one HEADER and nested chapters. * Properly handle svg resource requests and when there are part anchors that are clickable, make sure we handle them in the UI by adding a kavita-page handler. * Add Chapter group page even if one isn't set by using first page (without part) from nestedChildren. * Added extra debug code for issue #163. * Added new user preferences for books and updated the css so we scope it to our reading section. * Cleaned up style code * Implemented ability to save book preferences and some cleanup on existing apis. * Added an api for checking if a user has read something in a library type before. * Forgot to make sure the has reading progress is against a user lol. * Remove cacheservice code for books, sine we use an in-memory method * Handle svg images as well * Enhanced cover image extraction to check for a "cover" image if the cover image wasn't set in OPF before falling back to the first image. * Fixed an issue with special books not properly generating metadata due to not having filename set. * Cleanup, removed warmup task code from statup/program and changed taskscheduler to schedule tasks on startup only (or if tasks are changed from UI). * Code cleanup * Code cleanup * So much code. Lots of refactors to try to test scanner service. Moved a lot of the queries into Extensions to allow to easier test, even though it's hacky. Support @font-face src:url swaps with ' and ". Source summary information from epubs. * Well...baseURL needs to come from BE and not from UI lol. * Adjusted migrations so default values match Entity * Removed comment * I think I finally fixed #163! The issue was that when i checked if it had a parserInfo, i wasn't considering that the chapter range might have a - in it (0-6) and so when the code to check if range could parse out a number failed, it treated it like a special and checked range against info's filename. * Some bugfixes * Lots of testing, extracting code to make it easier to test. This code is buggy, but fixed a bug where 1) If we changed the normalization code, we would remove the whole db during a scan and 2) We weren't actually removing series properly. Other than that, code is being extracted to remove duplication and centralize logic. * More code cleanup and test cleanup to ensure scan loop is working as expected and matches expectaions from tests. * Cleaned up the code and made it so if I change normalization, which I do in this branch, it wont break existing DBs. * Some comic parser changes for partial chapter support. * Added some code for directory service and scanner service along with python code to generate test files (not used yet). Fixed up all the tests. * Code smells --- API.Tests/API.Tests.csproj | 2 + API.Tests/Entities/SeriesTest.cs | 29 + .../Extensions/ChapterListExtensionsTests.cs | 86 + .../Extensions/FileInfoExtensionsTests.cs | 27 + .../ParserInfoListExtensionsTests.cs | 42 + API.Tests/Extensions/SeriesExtensionsTests.cs | 10 +- API.Tests/Helpers/EntityFactory.cs | 57 + API.Tests/Helpers/ParserInfoFactory.cs | 25 + API.Tests/Helpers/PrivateObjectPrivateType.cs | 1864 ----------------- API.Tests/Helpers/TestCaseGenerator.cs | 55 + API.Tests/Parser/BookParserTests.cs | 15 + API.Tests/Parser/ComicParserTests.cs | 69 + .../MangaParserTests.cs} | 209 +- API.Tests/Parser/ParserInfoTests.cs | 110 + API.Tests/Parser/ParserTest.cs | 192 ++ API.Tests/Services/ArchiveServiceTests.cs | 4 +- API.Tests/Services/BackupServiceTests.cs | 47 - API.Tests/Services/BookServiceTests.cs | 32 + API.Tests/Services/CacheServiceTests.cs | 2 +- API.Tests/Services/DirectoryServiceTests.cs | 16 +- API.Tests/Services/ScannerServiceTests.cs | 141 +- ... Floes A Story of the Whaling Grounds.epub | Bin 0 -> 258623 bytes .../TestCases/Manga-testcase.txt | 153 ++ .../A_Town_Where_You_Live_omake.zip | 0 .../A_Town_Where_You_Live_v01.zip | 0 .../A_Town_Where_You_Live_v02.zip | 0 .../A_Town_Where_You_Live_v03.zip | 0 .../A_Town_Where_You_Live_v04.zip | 0 .../Manga/BEASTARS/BEASTARS v01 (digital).cbz | 0 .../Manga/BEASTARS/BEASTARS v02.cbz | 0 .../Manga/BEASTARS/BEASTARS v03.cbz | 0 .../Manga/BEASTARS/BEASTARS v04.cbz | 0 .../Manga/BEASTARS/BEASTARS v05.cbz | 0 .../Manga/BEASTARS/BEASTARS v06.cbz | 0 .../Manga/BEASTARS/BEASTARS v07.cbz | 0 .../Manga/BEASTARS/BEASTARS v08.cbz | 0 .../Manga/BEASTARS/BEASTARS v09.cbz | 0 .../Manga/BEASTARS/BEASTARS v10.cbz | 0 .../Manga/BEASTARS/BEASTARS v11.cbz | 0 .../Manga/BEASTARS/BEASTARS v12.cbz | 0 .../Manga/BEASTARS/BEASTARS v13.cbz | 0 .../Manga/BEASTARS/BEASTARS v14.cbz | 0 .../Manga/BEASTARS/BEASTARS v15.cbz | 0 .../Manga/BTOOOM!/Btooom! v01.cbz | 0 .../Manga/BTOOOM!/Btooom! v02.cbz | 0 .../Manga/BTOOOM!/Btooom! v03.cbz | 0 .../Manga/BTOOOM!/Btooom! v04.cbz | 0 .../Manga/BTOOOM!/Btooom! v05.cbz | 0 .../Manga/BTOOOM!/Btooom! v06.cbz | 0 .../Manga/BTOOOM!/Btooom! v07.cbz | 0 .../Manga/BTOOOM!/Btooom! v10.cbz | 0 .../TestCases/Manga-testcase.txt | 153 ++ API.Tests/generate_test_data.py | 80 + API/.dockerignore | 25 + API/API.csproj | 4 + API/Controllers/BookController.cs | 220 ++ API/Controllers/LibraryController.cs | 8 +- API/Controllers/ReaderController.cs | 2 +- API/Controllers/SettingsController.cs | 7 + API/Controllers/UsersController.cs | 14 +- API/DTOs/BookChapterItem.cs | 21 + API/DTOs/ChapterDto.cs | 4 + API/DTOs/UserPreferencesDto.cs | 9 +- API/Data/AppUserProgressRepository.cs | 24 + API/Data/DbFactory.cs | 54 + API/Data/LibraryRepository.cs | 14 +- ...19222000_BookReaderPreferences.Designer.cs | 748 +++++++ .../20210419222000_BookReaderPreferences.cs | 56 + ..._BookReaderPreferencesFontSize.Designer.cs | 751 +++++++ ...419234652_BookReaderPreferencesFontSize.cs | 24 + ...10423132900_CustomChapterTitle.Designer.cs | 751 +++++++ .../20210423132900_CustomChapterTitle.cs | 34 + .../Migrations/DataContextModelSnapshot.cs | 22 +- API/Dockerfile | 20 + API/Entities/AppUserPreferences.cs | 36 +- API/Entities/Chapter.cs | 19 + API/Entities/Enums/MangaFormat.cs | 4 +- API/Entities/MangaFile.cs | 8 + API/Entities/Series.cs | 1 + API/Entities/Volume.cs | 7 +- .../ApplicationServiceExtensions.cs | 5 +- API/Extensions/ChapterListExtensions.cs | 35 + API/Extensions/ParserInfoListExtensions.cs | 34 + API/Extensions/SeriesExtensions.cs | 11 +- API/Extensions/VolumeListExtensions.cs | 38 + API/Helpers/Converters/CronConverter.cs | 4 +- API/Interfaces/IAppUserProgressRepository.cs | 2 + API/Interfaces/IBookService.cs | 21 + API/Interfaces/ILibraryRepository.cs | 2 + API/Middleware/BookRedirectMiddleware.cs | 22 + API/Parser/Parser.cs | 94 +- API/Parser/ParserInfo.cs | 58 +- API/Program.cs | 8 - API/Services/ArchiveService.cs | 17 +- API/Services/BookService.cs | 257 +++ API/Services/CacheService.cs | 17 +- API/Services/DirectoryService.cs | 46 +- API/Services/MetadataService.cs | 49 +- API/Services/TaskScheduler.cs | 13 +- API/Services/Tasks/ScannerService.cs | 423 ++-- API/Startup.cs | 20 +- INSTALL.txt | 7 +- build.sh | 7 +- 103 files changed, 5017 insertions(+), 2480 deletions(-) create mode 100644 API.Tests/Entities/SeriesTest.cs create mode 100644 API.Tests/Extensions/ChapterListExtensionsTests.cs create mode 100644 API.Tests/Extensions/FileInfoExtensionsTests.cs create mode 100644 API.Tests/Extensions/ParserInfoListExtensionsTests.cs create mode 100644 API.Tests/Helpers/EntityFactory.cs create mode 100644 API.Tests/Helpers/ParserInfoFactory.cs delete mode 100644 API.Tests/Helpers/PrivateObjectPrivateType.cs create mode 100644 API.Tests/Helpers/TestCaseGenerator.cs create mode 100644 API.Tests/Parser/BookParserTests.cs create mode 100644 API.Tests/Parser/ComicParserTests.cs rename API.Tests/{ParserTest.cs => Parser/MangaParserTests.cs} (71%) create mode 100644 API.Tests/Parser/ParserInfoTests.cs create mode 100644 API.Tests/Parser/ParserTest.cs delete mode 100644 API.Tests/Services/BackupServiceTests.cs create mode 100644 API.Tests/Services/BookServiceTests.cs create mode 100644 API.Tests/Services/Test Data/BookService/EPUB/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub create mode 100644 API.Tests/Services/Test Data/DirectoryService/TestCases/Manga-testcase.txt create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_omake.zip create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v01.zip create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v02.zip create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v03.zip create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/A Town Where You Live/A_Town_Where_You_Live_v04.zip create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v01 (digital).cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v02.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v03.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v04.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v05.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v06.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v07.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v08.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v09.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v10.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v11.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v12.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v13.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v14.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BEASTARS/BEASTARS v15.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v01.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v02.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v03.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v04.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v05.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v06.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v07.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/Manga/BTOOOM!/Btooom! v10.cbz create mode 100644 API.Tests/Services/Test Data/ScannerService/TestCases/Manga-testcase.txt create mode 100644 API.Tests/generate_test_data.py create mode 100644 API/.dockerignore create mode 100644 API/Controllers/BookController.cs create mode 100644 API/DTOs/BookChapterItem.cs create mode 100644 API/Data/DbFactory.cs create mode 100644 API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs create mode 100644 API/Data/Migrations/20210419222000_BookReaderPreferences.cs create mode 100644 API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs create mode 100644 API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs create mode 100644 API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs create mode 100644 API/Data/Migrations/20210423132900_CustomChapterTitle.cs create mode 100644 API/Dockerfile create mode 100644 API/Extensions/ChapterListExtensions.cs create mode 100644 API/Extensions/ParserInfoListExtensions.cs create mode 100644 API/Extensions/VolumeListExtensions.cs create mode 100644 API/Interfaces/IBookService.cs create mode 100644 API/Middleware/BookRedirectMiddleware.cs create mode 100644 API/Services/BookService.cs diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index c1e0fa046..d486d9877 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -7,6 +7,7 @@ + @@ -26,6 +27,7 @@ + diff --git a/API.Tests/Entities/SeriesTest.cs b/API.Tests/Entities/SeriesTest.cs new file mode 100644 index 000000000..dd1f77b29 --- /dev/null +++ b/API.Tests/Entities/SeriesTest.cs @@ -0,0 +1,29 @@ +using System; +using API.Data; +using API.Tests.Helpers; +using Xunit; + +namespace API.Tests.Entities +{ + /// + /// Tests for + /// + public class SeriesTest + { + [Theory] + [InlineData("Darker than Black")] + public void CreateSeries(string name) + { + var key = API.Parser.Parser.Normalize(name); + var series = DbFactory.Series(name); + Assert.Equal(0, series.Id); + Assert.Equal(0, series.Pages); + Assert.Equal(name, series.Name); + Assert.Null(series.CoverImage); + Assert.Equal(name, series.LocalizedName); + Assert.Equal(name, series.SortName); + Assert.Equal(name, series.OriginalName); + Assert.Equal(key, series.NormalizedName); + } + } +} \ No newline at end of file diff --git a/API.Tests/Extensions/ChapterListExtensionsTests.cs b/API.Tests/Extensions/ChapterListExtensionsTests.cs new file mode 100644 index 000000000..2251c660b --- /dev/null +++ b/API.Tests/Extensions/ChapterListExtensionsTests.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using API.Entities; +using API.Entities.Enums; +using API.Extensions; +using API.Parser; +using Xunit; + +namespace API.Tests.Extensions +{ + public class ChapterListExtensionsTests + { + private Chapter CreateChapter(string range, string number, MangaFile file, bool isSpecial) + { + return new Chapter() + { + Range = range, + Number = number, + Files = new List() {file}, + IsSpecial = isSpecial + }; + } + + private MangaFile CreateFile(string file, MangaFormat format) + { + return new MangaFile() + { + FilePath = file, + Format = format + }; + } + + [Fact] + public void GetAnyChapterByRange_Test_ShouldBeNull() + { + var info = new ParserInfo() + { + Chapters = "0", + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = "/manga/darker than black.cbz", + Filename = "darker than black.cbz", + IsSpecial = false, + Series = "darker than black", + Title = "darker than black", + Volumes = "0" + }; + + var chapterList = new List() + { + CreateChapter("darker than black - Some special", "0", CreateFile("/manga/darker than black - special.cbz", MangaFormat.Archive), true) + }; + + var actualChapter = chapterList.GetChapterByRange(info); + + Assert.NotEqual(chapterList[0], actualChapter); + + } + + [Fact] + public void GetAnyChapterByRange_Test_ShouldBeNotNull() + { + var info = new ParserInfo() + { + Chapters = "0", + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = "/manga/darker than black.cbz", + Filename = "darker than black.cbz", + IsSpecial = true, + Series = "darker than black", + Title = "darker than black", + Volumes = "0" + }; + + var chapterList = new List() + { + CreateChapter("darker than black", "0", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true) + }; + + var actualChapter = chapterList.GetChapterByRange(info); + + Assert.Equal(chapterList[0], actualChapter); + + } + } +} \ No newline at end of file diff --git a/API.Tests/Extensions/FileInfoExtensionsTests.cs b/API.Tests/Extensions/FileInfoExtensionsTests.cs new file mode 100644 index 000000000..371d8ac76 --- /dev/null +++ b/API.Tests/Extensions/FileInfoExtensionsTests.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using API.Extensions; +using NSubstitute; +using Xunit; + +namespace API.Tests.Extensions +{ + public class FileInfoExtensionsTests + { + // [Fact] + // public void DoesLastWriteMatchTest() + // { + // var fi = Substitute.For(); + // fi.LastWriteTime = DateTime.Now; + // + // var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(1)); + // Assert.False(fi.DoesLastWriteMatch(deltaTime)); + // } + // + // [Fact] + // public void IsLastWriteLessThanTest() + // { + // + // } + } +} \ No newline at end of file diff --git a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs new file mode 100644 index 000000000..99f1383fc --- /dev/null +++ b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using API.Entities; +using API.Entities.Enums; +using API.Extensions; +using API.Parser; +using API.Tests.Helpers; +using Xunit; + +namespace API.Tests.Extensions +{ + public class ParserInfoListExtensions + { + [Theory] + [InlineData(new string[] {"1", "1", "3-5", "5", "8", "0", "0"}, new string[] {"1", "3-5", "5", "8", "0"})] + public void DistinctVolumesTest(string[] volumeNumbers, string[] expectedNumbers) + { + var infos = volumeNumbers.Select(n => new ParserInfo() {Volumes = n}).ToList(); + Assert.Equal(expectedNumbers, infos.DistinctVolumes()); + } + + [Theory] + [InlineData(new string[] {@"Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, new string[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, true)] + [InlineData(new string[] {@"Cynthia The Mission - c000-006 (v06-07) [Desudesu&Brolen].zip"}, new string[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, true)] + [InlineData(new string[] {@"Cynthia The Mission v20 c12-20 [Desudesu&Brolen].zip"}, new string[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, false)] + public void HasInfoTest(string[] inputInfos, string[] inputChapters, bool expectedHasInfo) + { + var infos = new List(); + foreach (var filename in inputInfos) + { + infos.Add(API.Parser.Parser.Parse( + filename, + string.Empty)); + } + + var files = inputChapters.Select(s => EntityFactory.CreateMangaFile(s, MangaFormat.Archive, 199)).ToList(); + var chapter = EntityFactory.CreateChapter("0-6", false, files); + + Assert.Equal(expectedHasInfo, infos.HasInfo(chapter)); + } + } +} \ No newline at end of file diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/API.Tests/Extensions/SeriesExtensionsTests.cs index 687ca9ca0..59c823fe1 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/API.Tests/Extensions/SeriesExtensionsTests.cs @@ -1,4 +1,5 @@ -using API.Entities; +using System; +using API.Entities; using API.Extensions; using Xunit; @@ -10,6 +11,11 @@ namespace API.Tests.Extensions [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker than Black"}, true)] [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker_than_Black"}, true)] [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker then Black!"}, false)] + [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"Salem's Lot"}, true)] + [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salems lot"}, true)] + [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salem's lot"}, true)] + // Different normalizations pass as we check normalization against an on-the-fly calculation so we don't delete series just because we change how normalization works + [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot", "salems lot"}, new [] {"salem's lot"}, true)] public void NameInListTest(string[] seriesInput, string[] list, bool expected) { var series = new Series() @@ -17,7 +23,7 @@ namespace API.Tests.Extensions Name = seriesInput[0], LocalizedName = seriesInput[1], OriginalName = seriesInput[2], - NormalizedName = Parser.Parser.Normalize(seriesInput[0]) + NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Parser.Parser.Normalize(seriesInput[0]) }; Assert.Equal(expected, series.NameInList(list)); diff --git a/API.Tests/Helpers/EntityFactory.cs b/API.Tests/Helpers/EntityFactory.cs new file mode 100644 index 000000000..b3b09d486 --- /dev/null +++ b/API.Tests/Helpers/EntityFactory.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using API.Entities; +using API.Entities.Enums; + +namespace API.Tests.Helpers +{ + /// + /// Used to help quickly create DB entities for Unit Testing + /// + public static class EntityFactory + { + public static Series CreateSeries(string name) + { + return new Series() + { + Name = name, + SortName = name, + LocalizedName = name, + NormalizedName = API.Parser.Parser.Normalize(name), + Volumes = new List() + }; + } + + public static Volume CreateVolume(string volumeNumber, List chapters = null) + { + return new Volume() + { + Name = volumeNumber, + Pages = 0, + Chapters = chapters ?? new List() + }; + } + + public static Chapter CreateChapter(string range, bool isSpecial, List files = null) + { + return new Chapter() + { + IsSpecial = isSpecial, + Range = range, + Number = API.Parser.Parser.MinimumNumberFromRange(range) + string.Empty, + Files = files ?? new List(), + Pages = 0, + + }; + } + + public static MangaFile CreateMangaFile(string filename, MangaFormat format, int pages) + { + return new MangaFile() + { + FilePath = filename, + Format = format, + Pages = pages + }; + } + } +} \ No newline at end of file diff --git a/API.Tests/Helpers/ParserInfoFactory.cs b/API.Tests/Helpers/ParserInfoFactory.cs new file mode 100644 index 000000000..7dcf564e1 --- /dev/null +++ b/API.Tests/Helpers/ParserInfoFactory.cs @@ -0,0 +1,25 @@ +using System.IO; +using API.Entities.Enums; +using API.Parser; + +namespace API.Tests.Helpers +{ + public static class ParserInfoFactory + { + public static ParserInfo CreateParsedInfo(string series, string volumes, string chapters, string filename, bool isSpecial) + { + return new ParserInfo() + { + Chapters = chapters, + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = Path.Join(@"/manga/", filename), + Filename = filename, + IsSpecial = isSpecial, + Title = Path.GetFileNameWithoutExtension(filename), + Series = series, + Volumes = volumes + }; + } + } +} \ No newline at end of file diff --git a/API.Tests/Helpers/PrivateObjectPrivateType.cs b/API.Tests/Helpers/PrivateObjectPrivateType.cs deleted file mode 100644 index e99016828..000000000 --- a/API.Tests/Helpers/PrivateObjectPrivateType.cs +++ /dev/null @@ -1,1864 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Reflection; - -namespace Microsoft.VisualStudio.TestTools.UnitTesting -{ - /// - /// This class represents the live NON public INTERNAL object in the system - /// - public class PrivateObject - { - // bind everything - private const BindingFlags BindToEveryThing = BindingFlags.Default | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public; - - private static BindingFlags constructorFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance | BindingFlags.NonPublic; - - private object target; // automatically initialized to null - private Type originalType; // automatically initialized to null - - private Dictionary> methodCache; // automatically initialized to null - - /// - /// Initializes a new instance of the class that contains - /// the already existing object of the private class - /// - /// object that serves as starting point to reach the private members - /// the derefrencing string using . that points to the object to be retrived as in m_X.m_Y.m_Z - public PrivateObject(object obj, string memberToAccess) - { - ValidateAccessString(memberToAccess); - - PrivateObject temp = obj as PrivateObject; - if (temp == null) - { - temp = new PrivateObject(obj); - } - - // Split The access string - string[] arr = memberToAccess.Split(new char[] { '.' }); - - for (int i = 0; i < arr.Length; i++) - { - object next = temp.InvokeHelper(arr[i], BindToEveryThing | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.GetProperty, null, CultureInfo.InvariantCulture); - temp = new PrivateObject(next); - } - - this.target = temp.target; - this.originalType = temp.originalType; - } - - /// - /// Initializes a new instance of the class that wraps the - /// specified type. - /// - /// Name of the assembly - /// fully qualified name - /// Argmenets to pass to the constructor - public PrivateObject(string assemblyName, string typeName, params object[] args) - : this(assemblyName, typeName, null, args) - { - } - - /// - /// Initializes a new instance of the class that wraps the - /// specified type. - /// - /// Name of the assembly - /// fully qualified name - /// An array of objects representing the number, order, and type of the parameters for the constructor to get - /// Argmenets to pass to the constructor - public PrivateObject(string assemblyName, string typeName, Type[] parameterTypes, object[] args) - : this(Type.GetType(string.Format(CultureInfo.InvariantCulture, "{0}, {1}", typeName, assemblyName), false), parameterTypes, args) - { - } - - /// - /// Initializes a new instance of the class that wraps the - /// specified type. - /// - /// type of the object to create - /// Argmenets to pass to the constructor - public PrivateObject(Type type, params object[] args) - : this(type, null, args) - { - } - - /// - /// Initializes a new instance of the class that wraps the - /// specified type. - /// - /// type of the object to create - /// An array of objects representing the number, order, and type of the parameters for the constructor to get - /// Argmenets to pass to the constructor - public PrivateObject(Type type, Type[] parameterTypes, object[] args) - { - object o; - if (parameterTypes != null) - { - ConstructorInfo ci = type.GetConstructor(BindToEveryThing, null, parameterTypes, null); - if (ci == null) - { - throw new ArgumentException("The constructor with the specified signature could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor."); - } - - try - { - o = ci.Invoke(args); - } - catch (TargetInvocationException e) - { - Debug.Assert(e.InnerException != null, "Inner exception should not be null."); - if (e.InnerException != null) - { - throw e.InnerException; - } - - throw; - } - } - else - { - o = Activator.CreateInstance(type, constructorFlags, null, args, null); - } - - this.ConstructFrom(o); - } - - /// - /// Initializes a new instance of the class that wraps - /// the given object. - /// - /// object to wrap - public PrivateObject(object obj) - { - this.ConstructFrom(obj); - } - - /// - /// Initializes a new instance of the class that wraps - /// the given object. - /// - /// object to wrap - /// PrivateType object - public PrivateObject(object obj, PrivateType type) - { - this.target = obj; - this.originalType = type.ReferencedType; - } - - /// - /// Gets or sets the target - /// - public object Target - { - get - { - return this.target; - } - - set - { - this.target = value; - this.originalType = value.GetType(); - } - } - - /// - /// Gets the type of underlying object - /// - public Type RealType - { - get - { - return this.originalType; - } - } - - private Dictionary> GenericMethodCache - { - get - { - if (this.methodCache == null) - { - this.BuildGenericMethodCacheForType(this.originalType); - } - - Debug.Assert(this.methodCache != null, "Invalid method cache for type."); - - return this.methodCache; - } - } - - /// - /// returns the hash code of the target object - /// - /// int representing hashcode of the target object - public override int GetHashCode() - { - Debug.Assert(this.target != null, "target should not be null."); - return this.target.GetHashCode(); - } - - /// - /// Equals - /// - /// Object with whom to compare - /// returns true if the objects are equal. - public override bool Equals(object obj) - { - if (this != obj) - { - Debug.Assert(this.target != null, "target should not be null."); - if (typeof(PrivateObject) == obj?.GetType()) - { - return this.target.Equals(((PrivateObject)obj).target); - } - else - { - return false; - } - } - - return true; - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// Arguments to pass to the member to invoke. - /// Result of method call - public object Invoke(string name, params object[] args) - { - return this.Invoke(name, null, args, CultureInfo.InvariantCulture); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// An array of objects representing the number, order, and type of the parameters for the method to get. - /// Arguments to pass to the member to invoke. - /// Result of method call - public object Invoke(string name, Type[] parameterTypes, object[] args) - { - return this.Invoke(name, parameterTypes, args, CultureInfo.InvariantCulture); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// An array of objects representing the number, order, and type of the parameters for the method to get. - /// Arguments to pass to the member to invoke. - /// An array of types corresponding to the types of the generic arguments. - /// Result of method call - public object Invoke(string name, Type[] parameterTypes, object[] args, Type[] typeArguments) - { - return this.Invoke(name, BindToEveryThing, parameterTypes, args, CultureInfo.InvariantCulture, typeArguments); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// Arguments to pass to the member to invoke. - /// Culture info - /// Result of method call - public object Invoke(string name, object[] args, CultureInfo culture) - { - return this.Invoke(name, null, args, culture); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// An array of objects representing the number, order, and type of the parameters for the method to get. - /// Arguments to pass to the member to invoke. - /// Culture info - /// Result of method call - public object Invoke(string name, Type[] parameterTypes, object[] args, CultureInfo culture) - { - return this.Invoke(name, BindToEveryThing, parameterTypes, args, culture); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// A bitmask comprised of one or more that specify how the search is conducted. - /// Arguments to pass to the member to invoke. - /// Result of method call - public object Invoke(string name, BindingFlags bindingFlags, params object[] args) - { - return this.Invoke(name, bindingFlags, null, args, CultureInfo.InvariantCulture); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// A bitmask comprised of one or more that specify how the search is conducted. - /// An array of objects representing the number, order, and type of the parameters for the method to get. - /// Arguments to pass to the member to invoke. - /// Result of method call - public object Invoke(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args) - { - return this.Invoke(name, bindingFlags, parameterTypes, args, CultureInfo.InvariantCulture); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// A bitmask comprised of one or more that specify how the search is conducted. - /// Arguments to pass to the member to invoke. - /// Culture info - /// Result of method call - public object Invoke(string name, BindingFlags bindingFlags, object[] args, CultureInfo culture) - { - return this.Invoke(name, bindingFlags, null, args, culture); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// A bitmask comprised of one or more that specify how the search is conducted. - /// An array of objects representing the number, order, and type of the parameters for the method to get. - /// Arguments to pass to the member to invoke. - /// Culture info - /// Result of method call - public object Invoke(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args, CultureInfo culture) - { - return this.Invoke(name, bindingFlags, parameterTypes, args, culture, null); - } - - /// - /// Invokes the specified method - /// - /// Name of the method - /// A bitmask comprised of one or more that specify how the search is conducted. - /// An array of objects representing the number, order, and type of the parameters for the method to get. - /// Arguments to pass to the member to invoke. - /// Culture info - /// An array of types corresponding to the types of the generic arguments. - /// Result of method call - public object Invoke(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args, CultureInfo culture, Type[] typeArguments) - { - if (parameterTypes != null) - { - bindingFlags |= BindToEveryThing | BindingFlags.Instance; - - // Fix up the parameter types - MethodInfo member = this.originalType.GetMethod(name, bindingFlags, null, parameterTypes, null); - - // If the method was not found and type arguments were provided for generic paramaters, - // attempt to look up a generic method. - if ((member == null) && (typeArguments != null)) - { - // This method may contain generic parameters...if so, the previous call to - // GetMethod() will fail because it doesn't fully support generic parameters. - - // Look in the method cache to see if there is a generic method - // on the incoming type that contains the correct signature. - member = this.GetGenericMethodFromCache(name, parameterTypes, typeArguments, bindingFlags, null); - } - - if (member == null) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); - } - - try - { - if (member.IsGenericMethodDefinition) - { - MethodInfo constructed = member.MakeGenericMethod(typeArguments); - return constructed.Invoke(this.target, bindingFlags, null, args, culture); - } - else - { - return member.Invoke(this.target, bindingFlags, null, args, culture); - } - } - catch (TargetInvocationException e) - { - Debug.Assert(e.InnerException != null, "Inner exception should not be null."); - if (e.InnerException != null) - { - throw e.InnerException; - } - - throw; - } - } - else - { - return this.InvokeHelper(name, bindingFlags | BindingFlags.InvokeMethod, args, culture); - } - } - - /// - /// Gets the array element using array of subsrcipts for each dimension - /// - /// Name of the member - /// the indices of array - /// An arrya of elements. - public object GetArrayElement(string name, params int[] indices) - { - return this.GetArrayElement(name, BindToEveryThing, indices); - } - - /// - /// Sets the array element using array of subsrcipts for each dimension - /// - /// Name of the member - /// Value to set - /// the indices of array - public void SetArrayElement(string name, object value, params int[] indices) - { - this.SetArrayElement(name, BindToEveryThing, value, indices); - } - - /// - /// Gets the array element using array of subsrcipts for each dimension - /// - /// Name of the member - /// A bitmask comprised of one or more that specify how the search is conducted. - /// the indices of array - /// An arrya of elements. - public object GetArrayElement(string name, BindingFlags bindingFlags, params int[] indices) - { - Array arr = (Array)this.InvokeHelper(name, BindingFlags.GetField | bindingFlags, null, CultureInfo.InvariantCulture); - return arr.GetValue(indices); - } - - /// - /// Sets the array element using array of subsrcipts for each dimension - /// - /// Name of the member - /// A bitmask comprised of one or more that specify how the search is conducted. - /// Value to set - /// the indices of array - public void SetArrayElement(string name, BindingFlags bindingFlags, object value, params int[] indices) - { - Array arr = (Array)this.InvokeHelper(name, BindingFlags.GetField | bindingFlags, null, CultureInfo.InvariantCulture); - arr.SetValue(value, indices); - } - - /// - /// Get the field - /// - /// Name of the field - /// The field. - public object GetField(string name) - { - return this.GetField(name, BindToEveryThing); - } - - /// - /// Sets the field - /// - /// Name of the field - /// value to set - public void SetField(string name, object value) - { - this.SetField(name, BindToEveryThing, value); - } - - /// - /// Gets the field - /// - /// Name of the field - /// A bitmask comprised of one or more that specify how the search is conducted. - /// The field. - public object GetField(string name, BindingFlags bindingFlags) - { - return this.InvokeHelper(name, BindingFlags.GetField | bindingFlags, null, CultureInfo.InvariantCulture); - } - - /// - /// Sets the field - /// - /// Name of the field - /// A bitmask comprised of one or more that specify how the search is conducted. - /// value to set - public void SetField(string name, BindingFlags bindingFlags, object value) - { - this.InvokeHelper(name, BindingFlags.SetField | bindingFlags, new object[] { value }, CultureInfo.InvariantCulture); - } - - /// - /// Get the field or property - /// - /// Name of the field or property - /// The field or property. - public object GetFieldOrProperty(string name) - { - return this.GetFieldOrProperty(name, BindToEveryThing); - } - - /// - /// Sets the field or property - /// - /// Name of the field or property - /// value to set - public void SetFieldOrProperty(string name, object value) - { - this.SetFieldOrProperty(name, BindToEveryThing, value); - } - - /// - /// Gets the field or property - /// - /// Name of the field or property - /// A bitmask comprised of one or more that specify how the search is conducted. - /// The field or property. - public object GetFieldOrProperty(string name, BindingFlags bindingFlags) - { - return this.InvokeHelper(name, BindingFlags.GetField | BindingFlags.GetProperty | bindingFlags, null, CultureInfo.InvariantCulture); - } - - /// - /// Sets the field or property - /// - /// Name of the field or property - /// A bitmask comprised of one or more that specify how the search is conducted. - /// value to set - public void SetFieldOrProperty(string name, BindingFlags bindingFlags, object value) - { - this.InvokeHelper(name, BindingFlags.SetField | BindingFlags.SetProperty | bindingFlags, new object[] { value }, CultureInfo.InvariantCulture); - } - - /// - /// Gets the property - /// - /// Name of the property - /// Arguments to pass to the member to invoke. - /// The property. - public object GetProperty(string name, params object[] args) - { - return this.GetProperty(name, null, args); - } - - /// - /// Gets the property - /// - /// Name of the property - /// An array of objects representing the number, order, and type of the parameters for the indexed property. - /// Arguments to pass to the member to invoke. - /// The property. - public object GetProperty(string name, Type[] parameterTypes, object[] args) - { - return this.GetProperty(name, BindToEveryThing, parameterTypes, args); - } - - /// - /// Set the property - /// - /// Name of the property - /// value to set - /// Arguments to pass to the member to invoke. - public void SetProperty(string name, object value, params object[] args) - { - this.SetProperty(name, null, value, args); - } - - /// - /// Set the property - /// - /// Name of the property - /// An array of objects representing the number, order, and type of the parameters for the indexed property. - /// value to set - /// Arguments to pass to the member to invoke. - public void SetProperty(string name, Type[] parameterTypes, object value, object[] args) - { - this.SetProperty(name, BindToEveryThing, value, parameterTypes, args); - } - - /// - /// Gets the property - /// - /// Name of the property - /// A bitmask comprised of one or more that specify how the search is conducted. - /// Arguments to pass to the member to invoke. - /// The property. - public object GetProperty(string name, BindingFlags bindingFlags, params object[] args) - { - return this.GetProperty(name, bindingFlags, null, args); - } - - /// - /// Gets the property - /// - /// Name of the property - /// A bitmask comprised of one or more that specify how the search is conducted. - /// An array of objects representing the number, order, and type of the parameters for the indexed property. - /// Arguments to pass to the member to invoke. - /// The property. - public object GetProperty(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args) - { - if (parameterTypes != null) - { - PropertyInfo pi = this.originalType.GetProperty(name, bindingFlags, null, null, parameterTypes, null); - if (pi == null) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); - } - - return pi.GetValue(this.target, args); - } - else - { - return this.InvokeHelper(name, bindingFlags | BindingFlags.GetProperty, args, null); - } - } - - /// - /// Sets the property - /// - /// Name of the property - /// A bitmask comprised of one or more that specify how the search is conducted. - /// value to set - /// Arguments to pass to the member to invoke. - public void SetProperty(string name, BindingFlags bindingFlags, object value, params object[] args) - { - this.SetProperty(name, bindingFlags, value, null, args); - } - - /// - /// Sets the property - /// - /// Name of the property - /// A bitmask comprised of one or more that specify how the search is conducted. - /// value to set - /// An array of objects representing the number, order, and type of the parameters for the indexed property. - /// Arguments to pass to the member to invoke. - public void SetProperty(string name, BindingFlags bindingFlags, object value, Type[] parameterTypes, object[] args) - { - if (parameterTypes != null) - { - PropertyInfo pi = this.originalType.GetProperty(name, bindingFlags, null, null, parameterTypes, null); - if (pi == null) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); - } - - pi.SetValue(this.target, value, args); - } - else - { - object[] pass = new object[(args?.Length ?? 0) + 1]; - pass[0] = value; - args?.CopyTo(pass, 1); - this.InvokeHelper(name, bindingFlags | BindingFlags.SetProperty, pass, null); - } - } - - /// - /// Validate access string - /// - /// access string - private static void ValidateAccessString(string access) - { - if (access.Length == 0) - { - throw new ArgumentException("Access string has invalid syntax."); - } - - string[] arr = access.Split('.'); - foreach (string str in arr) - { - if ((str.Length == 0) || (str.IndexOfAny(new char[] { ' ', '\t', '\n' }) != -1)) - { - throw new ArgumentException("Access string has invalid syntax."); - } - } - } - - /// - /// Invokes the memeber - /// - /// Name of the member - /// Additional attributes - /// Arguments for the invocation - /// Culture - /// Result of the invocation - private object InvokeHelper(string name, BindingFlags bindingFlags, object[] args, CultureInfo culture) - { - Debug.Assert(this.target != null, "Internal Error: Null reference is returned for internal object"); - - // Invoke the actual Method - try - { - return this.originalType.InvokeMember(name, bindingFlags, null, this.target, args, culture); - } - catch (TargetInvocationException e) - { - Debug.Assert(e.InnerException != null, "Inner exception should not be null."); - if (e.InnerException != null) - { - throw e.InnerException; - } - - throw; - } - } - - private void ConstructFrom(object obj) - { - this.target = obj; - this.originalType = obj.GetType(); - } - - private void BuildGenericMethodCacheForType(Type t) - { - Debug.Assert(t != null, "type should not be null."); - this.methodCache = new Dictionary>(); - - MethodInfo[] members = t.GetMethods(BindToEveryThing); - LinkedList listByName; // automatically initialized to null - - foreach (MethodInfo member in members) - { - if (member.IsGenericMethod || member.IsGenericMethodDefinition) - { - if (!this.GenericMethodCache.TryGetValue(member.Name, out listByName)) - { - listByName = new LinkedList(); - this.GenericMethodCache.Add(member.Name, listByName); - } - - Debug.Assert(listByName != null, "list should not be null."); - listByName.AddLast(member); - } - } - } - - /// - /// Extracts the most appropriate generic method signature from the current private type. - /// - /// The name of the method in which to search the signature cache. - /// An array of types corresponding to the types of the parameters in which to search. - /// An array of types corresponding to the types of the generic arguments. - /// to further filter the method signatures. - /// Modifiers for parameters. - /// A methodinfo instance. - private MethodInfo GetGenericMethodFromCache(string methodName, Type[] parameterTypes, Type[] typeArguments, BindingFlags bindingFlags, ParameterModifier[] modifiers) - { - Debug.Assert(!string.IsNullOrEmpty(methodName), "Invalid method name."); - Debug.Assert(parameterTypes != null, "Invalid parameter type array."); - Debug.Assert(typeArguments != null, "Invalid type arguments array."); - - // Build a preliminary list of method candidates that contain roughly the same signature. - var methodCandidates = this.GetMethodCandidates(methodName, parameterTypes, typeArguments, bindingFlags, modifiers); - - // Search of ambiguous methods (methods with the same signature). - MethodInfo[] finalCandidates = new MethodInfo[methodCandidates.Count]; - methodCandidates.CopyTo(finalCandidates, 0); - - if ((parameterTypes != null) && (parameterTypes.Length == 0)) - { - for (int i = 0; i < finalCandidates.Length; i++) - { - MethodInfo methodInfo = finalCandidates[i]; - - if (!RuntimeTypeHelper.CompareMethodSigAndName(methodInfo, finalCandidates[0])) - { - throw new AmbiguousMatchException(); - } - } - - // All the methods have the exact same name and sig so return the most derived one. - return RuntimeTypeHelper.FindMostDerivedNewSlotMeth(finalCandidates, finalCandidates.Length) as MethodInfo; - } - - // Now that we have a preliminary list of candidates, select the most appropriate one. - return RuntimeTypeHelper.SelectMethod(bindingFlags, finalCandidates, parameterTypes, modifiers) as MethodInfo; - } - - private LinkedList GetMethodCandidates(string methodName, Type[] parameterTypes, Type[] typeArguments, BindingFlags bindingFlags, ParameterModifier[] modifiers) - { - Debug.Assert(!string.IsNullOrEmpty(methodName), "methodName should not be null."); - Debug.Assert(parameterTypes != null, "parameterTypes should not be null."); - Debug.Assert(typeArguments != null, "typeArguments should not be null."); - - LinkedList methodCandidates = new LinkedList(); - LinkedList methods = null; - - if (!this.GenericMethodCache.TryGetValue(methodName, out methods)) - { - return methodCandidates; - } - - Debug.Assert(methods != null, "methods should not be null."); - - foreach (MethodInfo candidate in methods) - { - bool paramMatch = true; - ParameterInfo[] candidateParams = null; - Type[] genericArgs = candidate.GetGenericArguments(); - Type sourceParameterType = null; - - if (genericArgs.Length != typeArguments.Length) - { - continue; - } - - // Since we can't just get the correct MethodInfo from Reflection, - // we will just match the number of parameters, their order, and their type - var methodCandidate = candidate; - candidateParams = methodCandidate.GetParameters(); - - if (candidateParams.Length != parameterTypes.Length) - { - continue; - } - - // Exact binding - if ((bindingFlags & BindingFlags.ExactBinding) != 0) - { - int i = 0; - - foreach (ParameterInfo candidateParam in candidateParams) - { - sourceParameterType = parameterTypes[i++]; - - if (candidateParam.ParameterType.ContainsGenericParameters) - { - // Since we have a generic parameter here, just make sure the IsArray matches. - if (candidateParam.ParameterType.IsArray != sourceParameterType.IsArray) - { - paramMatch = false; - break; - } - } - else - { - if (candidateParam.ParameterType != sourceParameterType) - { - paramMatch = false; - break; - } - } - } - - if (paramMatch) - { - methodCandidates.AddLast(methodCandidate); - continue; - } - } - else - { - methodCandidates.AddLast(methodCandidate); - } - } - - return methodCandidates; - } - } - - /// - /// This class represents a private class for the Private Accessor functionality. - /// - public class PrivateType - { - /// - /// Binds to everything - /// - private const BindingFlags BindToEveryThing = BindingFlags.Default - | BindingFlags.NonPublic | BindingFlags.Instance - | BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy; - - /// - /// The wrapped type. - /// - private Type type; - - /// - /// Initializes a new instance of the class that contains the private type. - /// - /// Assembly name - /// fully qualified name of the - public PrivateType(string assemblyName, string typeName) - { - Assembly asm = Assembly.Load(assemblyName); - - this.type = asm.GetType(typeName, true); - } - - /// - /// Initializes a new instance of the class that contains - /// the private type from the type object - /// - /// The wrapped Type to create. - public PrivateType(Type type) - { - if (type == null) - { - throw new ArgumentNullException("type"); - } - - this.type = type; - } - - /// - /// Gets the referenced type - /// - public Type ReferencedType => this.type; - - /// - /// Invokes static member - /// - /// Name of the member to InvokeHelper - /// Arguements to the invoction - /// Result of invocation - public object InvokeStatic(string name, params object[] args) - { - return this.InvokeStatic(name, null, args, CultureInfo.InvariantCulture); - } - - /// - /// Invokes static member - /// - /// Name of the member to InvokeHelper - /// An array of objects representing the number, order, and type of the parameters for the method to invoke - /// Arguements to the invoction - /// Result of invocation - public object InvokeStatic(string name, Type[] parameterTypes, object[] args) - { - return this.InvokeStatic(name, parameterTypes, args, CultureInfo.InvariantCulture); - } - - /// - /// Invokes static member - /// - /// Name of the member to InvokeHelper - /// An array of objects representing the number, order, and type of the parameters for the method to invoke - /// Arguements to the invoction - /// An array of types corresponding to the types of the generic arguments. - /// Result of invocation - public object InvokeStatic(string name, Type[] parameterTypes, object[] args, Type[] typeArguments) - { - return this.InvokeStatic(name, BindToEveryThing, parameterTypes, args, CultureInfo.InvariantCulture, typeArguments); - } - - /// - /// Invokes the static method - /// - /// Name of the member - /// Arguements to the invocation - /// Culture - /// Result of invocation - public object InvokeStatic(string name, object[] args, CultureInfo culture) - { - return this.InvokeStatic(name, null, args, culture); - } - - /// - /// Invokes the static method - /// - /// Name of the member - /// An array of objects representing the number, order, and type of the parameters for the method to invoke - /// Arguements to the invocation - /// Culture info - /// Result of invocation - public object InvokeStatic(string name, Type[] parameterTypes, object[] args, CultureInfo culture) - { - return this.InvokeStatic(name, BindingFlags.InvokeMethod, parameterTypes, args, culture); - } - - /// - /// Invokes the static method - /// - /// Name of the member - /// Additional invocation attributes - /// Arguements to the invocation - /// Result of invocation - public object InvokeStatic(string name, BindingFlags bindingFlags, params object[] args) - { - return this.InvokeStatic(name, bindingFlags, null, args, CultureInfo.InvariantCulture); - } - - /// - /// Invokes the static method - /// - /// Name of the member - /// Additional invocation attributes - /// An array of objects representing the number, order, and type of the parameters for the method to invoke - /// Arguements to the invocation - /// Result of invocation - public object InvokeStatic(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args) - { - return this.InvokeStatic(name, bindingFlags, parameterTypes, args, CultureInfo.InvariantCulture); - } - - /// - /// Invokes the static method - /// - /// Name of the member - /// Additional invocation attributes - /// Arguements to the invocation - /// Culture - /// Result of invocation - public object InvokeStatic(string name, BindingFlags bindingFlags, object[] args, CultureInfo culture) - { - return this.InvokeStatic(name, bindingFlags, null, args, culture); - } - - /// - /// Invokes the static method - /// - /// Name of the member - /// Additional invocation attributes - /// /// An array of objects representing the number, order, and type of the parameters for the method to invoke - /// Arguements to the invocation - /// Culture - /// Result of invocation - public object InvokeStatic(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args, CultureInfo culture) - { - return this.InvokeStatic(name, bindingFlags, parameterTypes, args, culture, null); - } - - /// - /// Invokes the static method - /// - /// Name of the member - /// Additional invocation attributes - /// /// An array of objects representing the number, order, and type of the parameters for the method to invoke - /// Arguements to the invocation - /// Culture - /// An array of types corresponding to the types of the generic arguments. - /// Result of invocation - public object InvokeStatic(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args, CultureInfo culture, Type[] typeArguments) - { - if (parameterTypes != null) - { - MethodInfo member = this.type.GetMethod(name, bindingFlags | BindToEveryThing | BindingFlags.Static, null, parameterTypes, null); - if (member == null) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); - } - - try - { - if (member.IsGenericMethodDefinition) - { - MethodInfo constructed = member.MakeGenericMethod(typeArguments); - return constructed.Invoke(null, bindingFlags, null, args, culture); - } - else - { - return member.Invoke(null, bindingFlags, null, args, culture); - } - } - catch (TargetInvocationException e) - { - Debug.Assert(e.InnerException != null, "Inner Exception should not be null."); - if (e.InnerException != null) - { - throw e.InnerException; - } - - throw; - } - } - else - { - return this.InvokeHelperStatic(name, bindingFlags | BindingFlags.InvokeMethod, args, culture); - } - } - - /// - /// Gets the element in static array - /// - /// Name of the array - /// - /// A one-dimensional array of 32-bit integers that represent the indexes specifying - /// the position of the element to get. For instance, to access a[10][11] the indices would be {10,11} - /// - /// element at the specified location - public object GetStaticArrayElement(string name, params int[] indices) - { - return this.GetStaticArrayElement(name, BindToEveryThing, indices); - } - - /// - /// Sets the memeber of the static array - /// - /// Name of the array - /// value to set - /// - /// A one-dimensional array of 32-bit integers that represent the indexes specifying - /// the position of the element to set. For instance, to access a[10][11] the array would be {10,11} - /// - public void SetStaticArrayElement(string name, object value, params int[] indices) - { - this.SetStaticArrayElement(name, BindToEveryThing, value, indices); - } - - /// - /// Gets the element in satatic array - /// - /// Name of the array - /// Additional InvokeHelper attributes - /// - /// A one-dimensional array of 32-bit integers that represent the indexes specifying - /// the position of the element to get. For instance, to access a[10][11] the array would be {10,11} - /// - /// element at the spcified location - public object GetStaticArrayElement(string name, BindingFlags bindingFlags, params int[] indices) - { - Array arr = (Array)this.InvokeHelperStatic(name, BindingFlags.GetField | BindingFlags.GetProperty | bindingFlags, null, CultureInfo.InvariantCulture); - return arr.GetValue(indices); - } - - /// - /// Sets the memeber of the static array - /// - /// Name of the array - /// Additional InvokeHelper attributes - /// value to set - /// - /// A one-dimensional array of 32-bit integers that represent the indexes specifying - /// the position of the element to set. For instance, to access a[10][11] the array would be {10,11} - /// - public void SetStaticArrayElement(string name, BindingFlags bindingFlags, object value, params int[] indices) - { - Array arr = (Array)this.InvokeHelperStatic(name, BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Static | bindingFlags, null, CultureInfo.InvariantCulture); - arr.SetValue(value, indices); - } - - /// - /// Gets the static field - /// - /// Name of the field - /// The static field. - public object GetStaticField(string name) - { - return this.GetStaticField(name, BindToEveryThing); - } - - /// - /// Sets the static field - /// - /// Name of the field - /// Arguement to the invocation - public void SetStaticField(string name, object value) - { - this.SetStaticField(name, BindToEveryThing, value); - } - - /// - /// Gets the static field using specified InvokeHelper attributes - /// - /// Name of the field - /// Additional invocation attributes - /// The static field. - public object GetStaticField(string name, BindingFlags bindingFlags) - { - return this.InvokeHelperStatic(name, BindingFlags.GetField | BindingFlags.Static | bindingFlags, null, CultureInfo.InvariantCulture); - } - - /// - /// Sets the static field using binding attributes - /// - /// Name of the field - /// Additional InvokeHelper attributes - /// Arguement to the invocation - public void SetStaticField(string name, BindingFlags bindingFlags, object value) - { - this.InvokeHelperStatic(name, BindingFlags.SetField | bindingFlags | BindingFlags.Static, new[] { value }, CultureInfo.InvariantCulture); - } - - /// - /// Gets the static field or property - /// - /// Name of the field or property - /// The static field or property. - public object GetStaticFieldOrProperty(string name) - { - return this.GetStaticFieldOrProperty(name, BindToEveryThing); - } - - /// - /// Sets the static field or property - /// - /// Name of the field or property - /// Value to be set to field or property - public void SetStaticFieldOrProperty(string name, object value) - { - this.SetStaticFieldOrProperty(name, BindToEveryThing, value); - } - - /// - /// Gets the static field or property using specified InvokeHelper attributes - /// - /// Name of the field or property - /// Additional invocation attributes - /// The static field or property. - public object GetStaticFieldOrProperty(string name, BindingFlags bindingFlags) - { - return this.InvokeHelperStatic(name, BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Static | bindingFlags, null, CultureInfo.InvariantCulture); - } - - /// - /// Sets the static field or property using binding attributes - /// - /// Name of the field or property - /// Additional invocation attributes - /// Value to be set to field or property - public void SetStaticFieldOrProperty(string name, BindingFlags bindingFlags, object value) - { - this.InvokeHelperStatic(name, BindingFlags.SetField | BindingFlags.SetProperty | bindingFlags | BindingFlags.Static, new[] { value }, CultureInfo.InvariantCulture); - } - - /// - /// Gets the static property - /// - /// Name of the field or property - /// Arguements to the invocation - /// The static property. - public object GetStaticProperty(string name, params object[] args) - { - return this.GetStaticProperty(name, BindToEveryThing, args); - } - - /// - /// Sets the static property - /// - /// Name of the property - /// Value to be set to field or property - /// Arguments to pass to the member to invoke. - public void SetStaticProperty(string name, object value, params object[] args) - { - this.SetStaticProperty(name, BindToEveryThing, value, null, args); - } - - /// - /// Sets the static property - /// - /// Name of the property - /// Value to be set to field or property - /// An array of objects representing the number, order, and type of the parameters for the indexed property. - /// Arguments to pass to the member to invoke. - public void SetStaticProperty(string name, object value, Type[] parameterTypes, object[] args) - { - this.SetStaticProperty(name, BindingFlags.SetProperty, value, parameterTypes, args); - } - - /// - /// Gets the static property - /// - /// Name of the property - /// Additional invocation attributes. - /// Arguments to pass to the member to invoke. - /// The static property. - public object GetStaticProperty(string name, BindingFlags bindingFlags, params object[] args) - { - return this.GetStaticProperty(name, BindingFlags.GetProperty | BindingFlags.Static | bindingFlags, null, args); - } - - /// - /// Gets the static property - /// - /// Name of the property - /// Additional invocation attributes. - /// An array of objects representing the number, order, and type of the parameters for the indexed property. - /// Arguments to pass to the member to invoke. - /// The static property. - public object GetStaticProperty(string name, BindingFlags bindingFlags, Type[] parameterTypes, object[] args) - { - if (parameterTypes != null) - { - PropertyInfo pi = this.type.GetProperty(name, bindingFlags | BindingFlags.Static, null, null, parameterTypes, null); - if (pi == null) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); - } - - return pi.GetValue(null, args); - } - else - { - return this.InvokeHelperStatic(name, bindingFlags | BindingFlags.GetProperty, args, null); - } - } - - /// - /// Sets the static property - /// - /// Name of the property - /// Additional invocation attributes. - /// Value to be set to field or property - /// Optional index values for indexed properties. The indexes of indexed properties are zero-based. This value should be null for non-indexed properties. - public void SetStaticProperty(string name, BindingFlags bindingFlags, object value, params object[] args) - { - this.SetStaticProperty(name, bindingFlags, value, null, args); - } - - /// - /// Sets the static property - /// - /// Name of the property - /// Additional invocation attributes. - /// Value to be set to field or property - /// An array of objects representing the number, order, and type of the parameters for the indexed property. - /// Arguments to pass to the member to invoke. - public void SetStaticProperty(string name, BindingFlags bindingFlags, object value, Type[] parameterTypes, object[] args) - { - if (parameterTypes != null) - { - PropertyInfo pi = this.type.GetProperty(name, bindingFlags | BindingFlags.Static, null, null, parameterTypes, null); - if (pi == null) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, "The member specified ({0}) could not be found. You might need to regenerate your private accessor, or the member may be private and defined on a base class. If the latter is true, you need to pass the type that defines the member into PrivateObject's constructor.", name)); - } - - pi.SetValue(null, value, args); - } - else - { - object[] pass = new object[(args?.Length ?? 0) + 1]; - pass[0] = value; - args?.CopyTo(pass, 1); - this.InvokeHelperStatic(name, bindingFlags | BindingFlags.SetProperty, pass, null); - } - } - - /// - /// Invokes the static method - /// - /// Name of the member - /// Additional invocation attributes - /// Arguements to the invocation - /// Culture - /// Result of invocation - private object InvokeHelperStatic(string name, BindingFlags bindingFlags, object[] args, CultureInfo culture) - { - try - { - return this.type.InvokeMember(name, bindingFlags | BindToEveryThing | BindingFlags.Static, null, null, args, culture); - } - catch (TargetInvocationException e) - { - Debug.Assert(e.InnerException != null, "Inner Exception should not be null."); - if (e.InnerException != null) - { - throw e.InnerException; - } - - throw; - } - } - } - - /// - /// Provides method signature discovery for generic methods. - /// - internal class RuntimeTypeHelper - { - /// - /// Compares the method signatures of these two methods. - /// - /// Method1 - /// Method2 - /// True if they are similiar. - internal static bool CompareMethodSigAndName(MethodBase m1, MethodBase m2) - { - ParameterInfo[] params1 = m1.GetParameters(); - ParameterInfo[] params2 = m2.GetParameters(); - - if (params1.Length != params2.Length) - { - return false; - } - - int numParams = params1.Length; - for (int i = 0; i < numParams; i++) - { - if (params1[i].ParameterType != params2[i].ParameterType) - { - return false; - } - } - - return true; - } - - /// - /// Gets the hierarchy depth from the base type of the provided type. - /// - /// The type. - /// The depth. - internal static int GetHierarchyDepth(Type t) - { - int depth = 0; - - Type currentType = t; - do - { - depth++; - currentType = currentType.BaseType; - } - while (currentType != null); - - return depth; - } - - /// - /// Finds most dervied type with the provided information. - /// - /// Candidate matches. - /// Number of matches. - /// The most derived method. - internal static MethodBase FindMostDerivedNewSlotMeth(MethodBase[] match, int cMatches) - { - int deepestHierarchy = 0; - MethodBase methWithDeepestHierarchy = null; - - for (int i = 0; i < cMatches; i++) - { - // Calculate the depth of the hierarchy of the declaring type of the - // current method. - int currentHierarchyDepth = GetHierarchyDepth(match[i].DeclaringType); - - // Two methods with the same hierarchy depth are not allowed. This would - // mean that there are 2 methods with the same name and sig on a given type - // which is not allowed, unless one of them is vararg... - if (currentHierarchyDepth == deepestHierarchy) - { - if (methWithDeepestHierarchy != null) - { - Debug.Assert( - methWithDeepestHierarchy != null && ((match[i].CallingConvention & CallingConventions.VarArgs) - | (methWithDeepestHierarchy.CallingConvention & CallingConventions.VarArgs)) != 0, - "Calling conventions: " + match[i].CallingConvention + " - " + methWithDeepestHierarchy.CallingConvention); - } - - throw new AmbiguousMatchException(); - } - - // Check to see if this method is on the most derived class. - if (currentHierarchyDepth > deepestHierarchy) - { - deepestHierarchy = currentHierarchyDepth; - methWithDeepestHierarchy = match[i]; - } - } - - return methWithDeepestHierarchy; - } - - /// - /// Given a set of methods that match the base criteria, select a method based - /// upon an array of types. This method should return null if no method matches - /// the criteria. - /// - /// Binding specification. - /// Candidate matches - /// Types - /// Parameter modifiers. - /// Matching method. Null if none matches. - internal static MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers) - { - if (match == null) - { - throw new ArgumentNullException("match"); - } - - int i; - int j; - - Type[] realTypes = new Type[types.Length]; - for (i = 0; i < types.Length; i++) - { - realTypes[i] = types[i].UnderlyingSystemType; - } - - types = realTypes; - - // If there are no methods to match to, then return null, indicating that no method - // matches the criteria - if (match.Length == 0) - { - return null; - } - - // Find all the methods that can be described by the types parameter. - // Remove all of them that cannot. - int curIdx = 0; - for (i = 0; i < match.Length; i++) - { - ParameterInfo[] par = match[i].GetParameters(); - if (par.Length != types.Length) - { - continue; - } - - for (j = 0; j < types.Length; j++) - { - Type pCls = par[j].ParameterType; - - if (pCls.ContainsGenericParameters) - { - if (pCls.IsArray != types[j].IsArray) - { - break; - } - } - else - { - if (pCls == types[j]) - { - continue; - } - - if (pCls == typeof(object)) - { - continue; - } - else - { - if (!pCls.IsAssignableFrom(types[j])) - { - break; - } - } - } - } - - if (j == types.Length) - { - match[curIdx++] = match[i]; - } - } - - if (curIdx == 0) - { - return null; - } - - if (curIdx == 1) - { - return match[0]; - } - - // Walk all of the methods looking the most specific method to invoke - int currentMin = 0; - bool ambig = false; - int[] paramOrder = new int[types.Length]; - for (i = 0; i < types.Length; i++) - { - paramOrder[i] = i; - } - - for (i = 1; i < curIdx; i++) - { - int newMin = FindMostSpecificMethod(match[currentMin], paramOrder, null, match[i], paramOrder, null, types, null); - if (newMin == 0) - { - ambig = true; - } - else - { - if (newMin == 2) - { - currentMin = i; - ambig = false; - currentMin = i; - } - } - } - - if (ambig) - { - throw new AmbiguousMatchException(); - } - - return match[currentMin]; - } - - /// - /// Finds the most specific method in the two methods provided. - /// - /// Method 1 - /// Parameter order for Method 1 - /// Paramter array type. - /// Method 2 - /// Parameter order for Method 2 - /// >Paramter array type. - /// Types to search in. - /// Args. - /// An int representing the match. - internal static int FindMostSpecificMethod( - MethodBase m1, - int[] paramOrder1, - Type paramArrayType1, - MethodBase m2, - int[] paramOrder2, - Type paramArrayType2, - Type[] types, - object[] args) - { - // Find the most specific method based on the parameters. - int res = FindMostSpecific( - m1.GetParameters(), - paramOrder1, - paramArrayType1, - m2.GetParameters(), - paramOrder2, - paramArrayType2, - types, - args); - - // If the match was not ambiguous then return the result. - if (res != 0) - { - return res; - } - - // Check to see if the methods have the exact same name and signature. - if (CompareMethodSigAndName(m1, m2)) - { - // Determine the depth of the declaring types for both methods. - int hierarchyDepth1 = GetHierarchyDepth(m1.DeclaringType); - int hierarchyDepth2 = GetHierarchyDepth(m2.DeclaringType); - - // The most derived method is the most specific one. - if (hierarchyDepth1 == hierarchyDepth2) - { - return 0; - } - else if (hierarchyDepth1 < hierarchyDepth2) - { - return 2; - } - else - { - return 1; - } - } - - // The match is ambiguous. - return 0; - } - - /// - /// Finds the most specific method in the two methods provided. - /// - /// Method 1 - /// Parameter order for Method 1 - /// Paramter array type. - /// Method 2 - /// Parameter order for Method 2 - /// >Paramter array type. - /// Types to search in. - /// Args. - /// An int representing the match. - internal static int FindMostSpecific( - ParameterInfo[] p1, - int[] paramOrder1, - Type paramArrayType1, - ParameterInfo[] p2, - int[] paramOrder2, - Type paramArrayType2, - Type[] types, - object[] args) - { - // A method using params is always less specific than one not using params - if (paramArrayType1 != null && paramArrayType2 == null) - { - return 2; - } - - if (paramArrayType2 != null && paramArrayType1 == null) - { - return 1; - } - - bool p1Less = false; - bool p2Less = false; - - for (int i = 0; i < types.Length; i++) - { - if (args != null && args[i] == Type.Missing) - { - continue; - } - - Type c1, c2; - - // If a param array is present, then either - // the user re-ordered the parameters in which case - // the argument to the param array is either an array - // in which case the params is conceptually ignored and so paramArrayType1 == null - // or the argument to the param array is a single element - // in which case paramOrder[i] == p1.Length - 1 for that element - // or the user did not re-order the parameters in which case - // the paramOrder array could contain indexes larger than p.Length - 1 - //// so any index >= p.Length - 1 is being put in the param array - - if (paramArrayType1 != null && paramOrder1[i] >= p1.Length - 1) - { - c1 = paramArrayType1; - } - else - { - c1 = p1[paramOrder1[i]].ParameterType; - } - - if (paramArrayType2 != null && paramOrder2[i] >= p2.Length - 1) - { - c2 = paramArrayType2; - } - else - { - c2 = p2[paramOrder2[i]].ParameterType; - } - - if (c1 == c2) - { - continue; - } - - if (c1.ContainsGenericParameters || c2.ContainsGenericParameters) - { - continue; - } - - switch (FindMostSpecificType(c1, c2, types[i])) - { - case 0: - return 0; - case 1: - p1Less = true; - break; - case 2: - p2Less = true; - break; - } - } - - // Two way p1Less and p2Less can be equal. All the arguments are the - // same they both equal false, otherwise there were things that both - // were the most specific type on.... - if (p1Less == p2Less) - { - // it's possible that the 2 methods have same sig and default param in which case we match the one - // with the same number of args but only if they were exactly the same (that is p1Less and p2Lees are both false) - if (!p1Less && p1.Length != p2.Length && args != null) - { - if (p1.Length == args.Length) - { - return 1; - } - else if (p2.Length == args.Length) - { - return 2; - } - } - - return 0; - } - else - { - return (p1Less == true) ? 1 : 2; - } - } - - /// - /// Finds the most specific type in the two provided. - /// - /// Type 1 - /// Type 2 - /// The defining type - /// An int representing the match. - internal static int FindMostSpecificType(Type c1, Type c2, Type t) - { - // If the two types are exact move on... - if (c1 == c2) - { - return 0; - } - - if (c1 == t) - { - return 1; - } - - if (c2 == t) - { - return 2; - } - - bool c1FromC2; - bool c2FromC1; - - if (c1.IsByRef || c2.IsByRef) - { - if (c1.IsByRef && c2.IsByRef) - { - c1 = c1.GetElementType(); - c2 = c2.GetElementType(); - } - else if (c1.IsByRef) - { - if (c1.GetElementType() == c2) - { - return 2; - } - - c1 = c1.GetElementType(); - } - else - { - if (c2.GetElementType() == c1) - { - return 1; - } - - c2 = c2.GetElementType(); - } - } - - if (c1.IsPrimitive && c2.IsPrimitive) - { - c1FromC2 = true; - c2FromC1 = true; - } - else - { - c1FromC2 = c1.IsAssignableFrom(c2); - c2FromC1 = c2.IsAssignableFrom(c1); - } - - if (c1FromC2 == c2FromC1) - { - return 0; - } - - if (c1FromC2) - { - return 2; - } - else - { - return 1; - } - } - } -} \ No newline at end of file diff --git a/API.Tests/Helpers/TestCaseGenerator.cs b/API.Tests/Helpers/TestCaseGenerator.cs new file mode 100644 index 000000000..57923dfb2 --- /dev/null +++ b/API.Tests/Helpers/TestCaseGenerator.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.IO; +using API.Services; + +namespace API.Tests.Helpers +{ + /// + /// Given a -testcase.txt file, will generate a folder with fake archive or book files. These files are just renamed txt files. + /// This currently is broken - you cannot create files from a unit test it seems + /// + public static class TestCaseGenerator + { + public static string GenerateFiles(string directory, string fileToExpand) + { + //var files = Directory.GetFiles(directory, fileToExpand); + var file = new FileInfo(fileToExpand); + if (!file.Exists && file.Name.EndsWith("-testcase.txt")) return string.Empty; + + var baseDirectory = TestCaseGenerator.CreateTestBase(fileToExpand, directory); + var filesToCreate = File.ReadLines(file.FullName); + foreach (var fileToCreate in filesToCreate) + { + // var folders = DirectoryService.GetFoldersTillRoot(directory, fileToCreate); + // foreach (var VARIABLE in COLLECTION) + // { + // + // } + File.Create(fileToCreate); + } + + + + + return baseDirectory; + } + + /// + /// Creates and returns a new base directory for data creation for a given testcase + /// + /// + /// + /// + private static string CreateTestBase(string file, string rootDirectory) + { + var baseDir = file.Split("-testcase.txt")[0]; + var newDirectory = Path.Join(rootDirectory, baseDir); + if (!Directory.Exists(newDirectory)) + { + new DirectoryInfo(newDirectory).Create(); + } + + return newDirectory; + } + } +} \ No newline at end of file diff --git a/API.Tests/Parser/BookParserTests.cs b/API.Tests/Parser/BookParserTests.cs new file mode 100644 index 000000000..0a43b7c73 --- /dev/null +++ b/API.Tests/Parser/BookParserTests.cs @@ -0,0 +1,15 @@ +using API.Services; +using Xunit; + +namespace API.Tests.Parser +{ + public class BookParserTests + { + [Theory] + [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", "Gifting The Wonderful World With Blessings!")] + public void ParseSeriesTest(string filename, string expected) + { + Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); + } + } +} \ No newline at end of file diff --git a/API.Tests/Parser/ComicParserTests.cs b/API.Tests/Parser/ComicParserTests.cs new file mode 100644 index 000000000..9d91a5feb --- /dev/null +++ b/API.Tests/Parser/ComicParserTests.cs @@ -0,0 +1,69 @@ +using Xunit; + +namespace API.Tests.Parser +{ + public class ComicParserTests + { + [Theory] + [InlineData("01 Spider-Man & Wolverine 01.cbr", "Spider-Man & Wolverine")] + [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "Asterix the Gladiator")] + [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "The First Asterix Frieze")] + [InlineData("Batman & Catwoman - Trail of the Gun 01", "Batman & Catwoman - Trail of the Gun")] + [InlineData("Batman & Daredevil - King of New York", "Batman & Daredevil - King of New York")] + [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "Batman & Grendel")] + [InlineData("Batman & Robin the Teen Wonder #0", "Batman & Robin the Teen Wonder")] + [InlineData("Batman & Wildcat (1 of 3)", "Batman & Wildcat")] + [InlineData("Batman And Superman World's Finest #01", "Batman And Superman World's Finest")] + [InlineData("Babe 01", "Babe")] + [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "Scott Pilgrim")] + [InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "Teen Titans")] + [InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", "Scott Pilgrim")] + [InlineData("Wolverine - Origins 003 (2006) (digital) (Minutemen-PhD)", "Wolverine - Origins")] + [InlineData("Invincible Vol 01 Family matters (2005) (Digital).cbr", "Invincible")] + public void ParseComicSeriesTest(string filename, string expected) + { + Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename)); + } + + [Theory] + [InlineData("01 Spider-Man & Wolverine 01.cbr", "1")] + [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "4")] + [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")] + [InlineData("Batman & Catwoman - Trail of the Gun 01", "1")] + [InlineData("Batman & Daredevil - King of New York", "0")] + [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")] + [InlineData("Batman & Robin the Teen Wonder #0", "0")] + [InlineData("Batman & Wildcat (1 of 3)", "0")] + [InlineData("Batman And Superman World's Finest #01", "1")] + [InlineData("Babe 01", "1")] + [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "1")] + [InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")] + [InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", "2")] + [InlineData("Superman v1 024 (09-10 1943)", "1")] + public void ParseComicVolumeTest(string filename, string expected) + { + Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename)); + } + + [Theory] + [InlineData("01 Spider-Man & Wolverine 01.cbr", "0")] + [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")] + [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")] + [InlineData("Batman & Catwoman - Trail of the Gun 01", "0")] + [InlineData("Batman & Daredevil - King of New York", "0")] + [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")] + [InlineData("Batman & Robin the Teen Wonder #0", "0")] + [InlineData("Batman & Wildcat (1 of 3)", "1")] + [InlineData("Batman & Wildcat (2 of 3)", "2")] + [InlineData("Batman And Superman World's Finest #01", "0")] + [InlineData("Babe 01", "0")] + [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "1")] + [InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")] + [InlineData("Superman v1 024 (09-10 1943)", "24")] + [InlineData("Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr", "70.5")] + public void ParseComicChapterTest(string filename, string expected) + { + Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename)); + } + } +} \ No newline at end of file diff --git a/API.Tests/ParserTest.cs b/API.Tests/Parser/MangaParserTests.cs similarity index 71% rename from API.Tests/ParserTest.cs rename to API.Tests/Parser/MangaParserTests.cs index 3534e75f8..1b2d4500b 100644 --- a/API.Tests/ParserTest.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -1,18 +1,16 @@ -using System.Collections.Generic; +using System.Collections.Generic; using API.Entities.Enums; using API.Parser; using Xunit; using Xunit.Abstractions; -using static API.Parser.Parser; -namespace API.Tests +namespace API.Tests.Parser { - public class ParserTests + public class MangaParserTests { private readonly ITestOutputHelper _testOutputHelper; - - public ParserTests(ITestOutputHelper testOutputHelper) + public MangaParserTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } @@ -61,9 +59,10 @@ namespace API.Tests [InlineData("Gantz.V26.cbz", "26")] [InlineData("NEEDLESS_Vol.4_-Simeon_6_v2[SugoiSugoi].rar", "4")] [InlineData("[Hidoi]_Amaenaideyo_MS_vol01_chp02.rar", "1")] + [InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "4")] public void ParseVolumeTest(string filename, string expected) { - Assert.Equal(expected, ParseVolume(filename)); + Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename)); } [Theory] @@ -132,9 +131,10 @@ namespace API.Tests [InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "Umineko no Naku Koro ni")] [InlineData("Kimetsu no Yaiba - Digital Colored Comics c162 Three Victorious Stars.cbz", "Kimetsu no Yaiba - Digital Colored Comics")] [InlineData("[Hidoi]_Amaenaideyo_MS_vol01_chp02.rar", "Amaenaideyo MS")] + [InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "NEEDLESS")] public void ParseSeriesTest(string filename, string expected) { - Assert.Equal(expected, ParseSeries(filename)); + Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename)); } [Theory] @@ -193,51 +193,9 @@ namespace API.Tests [InlineData("[Hidoi]_Amaenaideyo_MS_vol01_chp02.rar", "2")] public void ParseChaptersTest(string filename, string expected) { - Assert.Equal(expected, ParseChapter(filename)); - } - - - [Theory] - [InlineData("0001", "1")] - [InlineData("1", "1")] - [InlineData("0013", "13")] - public void RemoveLeadingZeroesTest(string input, string expected) - { - Assert.Equal(expected, RemoveLeadingZeroes(input)); + Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename)); } - [Theory] - [InlineData("1", "001")] - [InlineData("10", "010")] - [InlineData("100", "100")] - [InlineData("4-8", "004-008")] - public void PadZerosTest(string input, string expected) - { - Assert.Equal(expected, PadZeros(input)); - } - - [Theory] - [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("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", "Kasumi Otoko no Ko v1.1")] - public void CleanTitleTest(string input, string expected) - { - Assert.Equal(expected, CleanTitle(input)); - } - - [Theory] - [InlineData("test.cbz", true)] - [InlineData("test.cbr", true)] - [InlineData("test.zip", true)] - [InlineData("test.rar", true)] - [InlineData("test.rar.!qb", false)] - [InlineData("[shf-ma-khs-aqs]negi_pa_vol15007.jpg", false)] - public void IsArchiveTest(string input, bool expected) - { - Assert.Equal(expected, IsArchive(input)); - } [Theory] [InlineData("Tenjou Tenge Omnibus", "Omnibus")] @@ -250,7 +208,7 @@ namespace API.Tests [InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "Full Color")] public void ParseEditionTest(string input, string expected) { - Assert.Equal(expected, ParseEdition(input)); + Assert.Equal(expected, API.Parser.Parser.ParseEdition(input)); } [Theory] [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", true)] @@ -260,151 +218,26 @@ namespace API.Tests [InlineData("Darker than Black Shikkoku no Hana Fanbook Extra [Simple Scans].zip", true)] [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", true)] [InlineData("Ani-Hina Art Collection.cbz", true)] + [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", true)] public void ParseMangaSpecialTest(string input, bool expected) { - Assert.Equal(expected, ParseMangaSpecial(input) != ""); + Assert.Equal(expected, !string.IsNullOrEmpty(API.Parser.Parser.ParseMangaSpecial(input))); } - [Theory] - [InlineData("12-14", 12)] - [InlineData("24", 24)] - [InlineData("18-04", 4)] - [InlineData("18-04.5", 4.5)] - [InlineData("40", 40)] - public void MinimumNumberFromRangeTest(string input, float expected) - { - Assert.Equal(expected, MinimumNumberFromRange(input)); - } - - [Theory] - [InlineData("Darker Than Black", "darkerthanblack")] - [InlineData("Darker Than Black - Something", "darkerthanblacksomething")] - [InlineData("Darker Than_Black", "darkerthanblack")] - [InlineData("", "")] - public void NormalizeTest(string input, string expected) - { - Assert.Equal(expected, Normalize(input)); - } - - [Theory] - [InlineData("01 Spider-Man & Wolverine 01.cbr", "Spider-Man & Wolverine")] - [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "Asterix the Gladiator")] - [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "The First Asterix Frieze")] - [InlineData("Batman & Catwoman - Trail of the Gun 01", "Batman & Catwoman - Trail of the Gun")] - [InlineData("Batman & Daredevil - King of New York", "Batman & Daredevil - King of New York")] - [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "Batman & Grendel")] - [InlineData("Batman & Robin the Teen Wonder #0", "Batman & Robin the Teen Wonder")] - [InlineData("Batman & Wildcat (1 of 3)", "Batman & Wildcat")] - [InlineData("Batman And Superman World's Finest #01", "Batman And Superman World's Finest")] - [InlineData("Babe 01", "Babe")] - [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "Scott Pilgrim")] - [InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "Teen Titans")] - [InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", "Scott Pilgrim")] - [InlineData("Wolverine - Origins 003 (2006) (digital) (Minutemen-PhD)", "Wolverine - Origins")] - [InlineData("Invincible Vol 01 Family matters (2005) (Digital).cbr", "Invincible")] - public void ParseComicSeriesTest(string filename, string expected) - { - Assert.Equal(expected, ParseComicSeries(filename)); - } - - [Theory] - [InlineData("01 Spider-Man & Wolverine 01.cbr", "1")] - [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "4")] - [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")] - [InlineData("Batman & Catwoman - Trail of the Gun 01", "1")] - [InlineData("Batman & Daredevil - King of New York", "0")] - [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")] - [InlineData("Batman & Robin the Teen Wonder #0", "0")] - [InlineData("Batman & Wildcat (1 of 3)", "0")] - [InlineData("Batman And Superman World's Finest #01", "1")] - [InlineData("Babe 01", "1")] - [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "1")] - [InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")] - [InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", "2")] - [InlineData("Superman v1 024 (09-10 1943)", "1")] - public void ParseComicVolumeTest(string filename, string expected) - { - Assert.Equal(expected, ParseComicVolume(filename)); - } - - [Theory] - [InlineData("01 Spider-Man & Wolverine 01.cbr", "0")] - [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")] - [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")] - [InlineData("Batman & Catwoman - Trail of the Gun 01", "0")] - [InlineData("Batman & Daredevil - King of New York", "0")] - [InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "0")] - [InlineData("Batman & Robin the Teen Wonder #0", "0")] - [InlineData("Batman & Wildcat (1 of 3)", "1")] - [InlineData("Batman & Wildcat (2 of 3)", "2")] - [InlineData("Batman And Superman World's Finest #01", "0")] - [InlineData("Babe 01", "0")] - [InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "0")] - [InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")] - [InlineData("Superman v1 024 (09-10 1943)", "24")] - public void ParseComicChapterTest(string filename, string expected) - { - Assert.Equal(expected, ParseComicChapter(filename)); - } - - [Theory] - [InlineData("test.jpg", true)] - [InlineData("test.jpeg", true)] - [InlineData("test.png", true)] - [InlineData(".test.jpg", false)] - [InlineData("!test.jpg", false)] - public void IsImageTest(string filename, bool expected) - { - Assert.Equal(expected, IsImage(filename)); - } - - [Theory] - [InlineData("C:/", "C:/Love Hina/Love Hina - Special.cbz", "Love Hina")] - [InlineData("C:/", "C:/Love Hina/Specials/Ani-Hina Art Collection.cbz", "Love Hina")] - [InlineData("C:/", "C:/Mujaki no Rakuen Something/Mujaki no Rakuen Vol12 ch76.cbz", "Mujaki no Rakuen")] - public void FallbackTest(string rootDir, string inputPath, string expectedSeries) - { - var actual = Parse(inputPath, rootDir); - if (actual == null) - { - Assert.NotNull(actual); - return; - } - - Assert.Equal(expectedSeries, actual.Series); - } - - [Theory] - [InlineData("Love Hina - Special.jpg", false)] - [InlineData("folder.jpg", true)] - [InlineData("DearS_v01_cover.jpg", true)] - [InlineData("DearS_v01_covers.jpg", false)] - [InlineData("!cover.jpg", true)] - [InlineData("cover.jpg", true)] - [InlineData("cover.png", true)] - [InlineData("ch1/cover.png", true)] - public void IsCoverImageTest(string inputPath, bool expected) - { - Assert.Equal(expected, IsCoverImage(inputPath)); - } - - [Theory] - [InlineData("__MACOSX/Love Hina - Special.jpg", true)] - [InlineData("TEST/Love Hina - Special.jpg", false)] - [InlineData("__macosx/Love Hina/", false)] - [InlineData("MACOSX/Love Hina/", false)] - public void HasBlacklistedFolderInPathTest(string inputPath, bool expected) - { - Assert.Equal(expected, HasBlacklistedFolderInPath(inputPath)); - } - [Theory] [InlineData("image.png", MangaFormat.Image)] [InlineData("image.cbz", MangaFormat.Archive)] [InlineData("image.txt", MangaFormat.Unknown)] public void ParseFormatTest(string inputFile, MangaFormat expected) { - Assert.Equal(expected, ParseFormat(inputFile)); + Assert.Equal(expected, API.Parser.Parser.ParseFormat(inputFile)); + } + + [Theory] + [InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown].epub", "Side Stories")] + public void ParseSpecialTest(string inputFile, string expected) + { + Assert.Equal(expected, API.Parser.Parser.ParseMangaSpecial(inputFile)); } [Fact] @@ -496,7 +329,7 @@ namespace API.Tests foreach (var file in expected.Keys) { var expectedInfo = expected[file]; - var actual = Parse(file, rootPath); + var actual = API.Parser.Parser.Parse(file, rootPath); if (expectedInfo == null) { Assert.Null(actual); diff --git a/API.Tests/Parser/ParserInfoTests.cs b/API.Tests/Parser/ParserInfoTests.cs new file mode 100644 index 000000000..78b879de7 --- /dev/null +++ b/API.Tests/Parser/ParserInfoTests.cs @@ -0,0 +1,110 @@ +using API.Entities.Enums; +using API.Parser; +using Xunit; + +namespace API.Tests.Parser +{ + public class ParserInfoTests + { + [Fact] + public void MergeFromTest() + { + var p1 = new ParserInfo() + { + Chapters = "0", + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = "/manga/darker than black.cbz", + IsSpecial = false, + Series = "darker than black", + Title = "darker than black", + Volumes = "0" + }; + + var p2 = new ParserInfo() + { + Chapters = "1", + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = "/manga/darker than black.cbz", + IsSpecial = false, + Series = "darker than black", + Title = "Darker Than Black", + Volumes = "0" + }; + + var expected = new ParserInfo() + { + Chapters = "1", + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = "/manga/darker than black.cbz", + IsSpecial = false, + Series = "darker than black", + Title = "darker than black", + Volumes = "0" + }; + p1.Merge(p2); + + AssertSame(expected, p1); + + } + + [Fact] + public void MergeFromTest2() + { + var p1 = new ParserInfo() + { + Chapters = "1", + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = "/manga/darker than black.cbz", + IsSpecial = true, + Series = "darker than black", + Title = "darker than black", + Volumes = "0" + }; + + var p2 = new ParserInfo() + { + Chapters = "0", + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = "/manga/darker than black.cbz", + IsSpecial = false, + Series = "darker than black", + Title = "Darker Than Black", + Volumes = "1" + }; + + var expected = new ParserInfo() + { + Chapters = "1", + Edition = "", + Format = MangaFormat.Archive, + FullFilePath = "/manga/darker than black.cbz", + IsSpecial = true, + Series = "darker than black", + Title = "darker than black", + Volumes = "1" + }; + p1.Merge(p2); + + AssertSame(expected, p1); + + } + + + private void AssertSame(ParserInfo expected, ParserInfo actual) + { + Assert.Equal(expected.Chapters, actual.Chapters); + Assert.Equal(expected.Volumes, actual.Volumes); + Assert.Equal(expected.Edition, actual.Edition); + Assert.Equal(expected.Filename, actual.Filename); + Assert.Equal(expected.Format, actual.Format); + Assert.Equal(expected.Series, actual.Series); + Assert.Equal(expected.IsSpecial, actual.IsSpecial); + Assert.Equal(expected.FullFilePath, actual.FullFilePath); + } + } +} \ No newline at end of file diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs new file mode 100644 index 000000000..2f46c6bb2 --- /dev/null +++ b/API.Tests/Parser/ParserTest.cs @@ -0,0 +1,192 @@ +using Xunit; +using static API.Parser.Parser; + +namespace API.Tests.Parser +{ + public class ParserTests + { + + [Theory] + [InlineData("0001", "1")] + [InlineData("1", "1")] + [InlineData("0013", "13")] + public void RemoveLeadingZeroesTest(string input, string expected) + { + Assert.Equal(expected, RemoveLeadingZeroes(input)); + } + + [Theory] + [InlineData("1", "001")] + [InlineData("10", "010")] + [InlineData("100", "100")] + public void PadZerosTest(string input, string expected) + { + Assert.Equal(expected, PadZeros(input)); + } + + [Theory] + [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("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", "Kasumi Otoko no Ko v1.1")] + public void CleanTitleTest(string input, string expected) + { + Assert.Equal(expected, CleanTitle(input)); + } + + + // [Theory] + // //[InlineData("@font-face{font-family:\"PaytoneOne\";src:url(\"..\\/Fonts\\/PaytoneOne.ttf\")}", "@font-face{font-family:\"PaytoneOne\";src:url(\"PaytoneOne.ttf\")}")] + // [InlineData("@font-face{font-family:\"PaytoneOne\";src:url(\"..\\/Fonts\\/PaytoneOne.ttf\")}", "..\\/Fonts\\/PaytoneOne.ttf")] + // //[InlineData("@font-face{font-family:'PaytoneOne';src:url('..\\/Fonts\\/PaytoneOne.ttf')}", "@font-face{font-family:'PaytoneOne';src:url('PaytoneOne.ttf')}")] + // //[InlineData("@font-face{\r\nfont-family:'PaytoneOne';\r\nsrc:url('..\\/Fonts\\/PaytoneOne.ttf')\r\n}", "@font-face{font-family:'PaytoneOne';src:url('PaytoneOne.ttf')}")] + // public void ReplaceStyleUrlTest(string input, string expected) + // { + // var replacementStr = "PaytoneOne.ttf"; + // // TODO: Use Match to validate since replace is weird + // //Assert.Equal(expected, FontSrcUrlRegex.Replace(input, "$1" + replacementStr + "$2" + "$3")); + // var match = FontSrcUrlRegex.Match(input); + // Assert.Equal(!string.IsNullOrEmpty(expected), FontSrcUrlRegex.Match(input).Success); + // } + + + [Theory] + [InlineData("test.cbz", true)] + [InlineData("test.cbr", true)] + [InlineData("test.zip", true)] + [InlineData("test.rar", true)] + [InlineData("test.rar.!qb", false)] + [InlineData("[shf-ma-khs-aqs]negi_pa_vol15007.jpg", false)] + public void IsArchiveTest(string input, bool expected) + { + Assert.Equal(expected, IsArchive(input)); + } + + [Theory] + [InlineData("test.epub", true)] + [InlineData("test.pdf", false)] + [InlineData("test.mobi", false)] + [InlineData("test.djvu", false)] + [InlineData("test.zip", false)] + [InlineData("test.rar", false)] + [InlineData("test.epub.!qb", false)] + [InlineData("[shf-ma-khs-aqs]negi_pa_vol15007.ebub", false)] + public void IsBookTest(string input, bool expected) + { + Assert.Equal(expected, IsBook(input)); + } + + [Theory] + [InlineData("test.epub", true)] + [InlineData("test.EPUB", true)] + [InlineData("test.mobi", false)] + [InlineData("test.epub.!qb", false)] + [InlineData("[shf-ma-khs-aqs]negi_pa_vol15007.ebub", false)] + public void IsEpubTest(string input, bool expected) + { + Assert.Equal(expected, IsEpub(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")] + // [InlineData("Wotakoi - Love is Hard for Otaku Omnibus v01 (2018) (Digital) (danke-Empire)", "Omnibus")] + // [InlineData("To Love Ru v01 Uncensored (Ch.001-007)", "Uncensored")] + // [InlineData("Chobits Omnibus Edition v01 [Dark Horse]", "Omnibus Edition")] + // [InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "Digital Colored Comics")] + // [InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "Full Color")] + // public void ParseEditionTest(string input, string expected) + // { + // Assert.Equal(expected, ParseEdition(input)); + // } + + // [Theory] + // [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", true)] + // [InlineData("Beelzebub_Omake_June_2012_RHS", true)] + // [InlineData("Beelzebub_Side_Story_02_RHS.zip", false)] + // [InlineData("Darker than Black Shikkoku no Hana Special [Simple Scans].zip", true)] + // [InlineData("Darker than Black Shikkoku no Hana Fanbook Extra [Simple Scans].zip", true)] + // [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", true)] + // [InlineData("Ani-Hina Art Collection.cbz", true)] + // public void ParseMangaSpecialTest(string input, bool expected) + // { + // Assert.Equal(expected, ParseMangaSpecial(input) != ""); + // } + + [Theory] + [InlineData("12-14", 12)] + [InlineData("24", 24)] + [InlineData("18-04", 4)] + [InlineData("18-04.5", 4.5)] + [InlineData("40", 40)] + public void MinimumNumberFromRangeTest(string input, float expected) + { + Assert.Equal(expected, MinimumNumberFromRange(input)); + } + + [Theory] + [InlineData("Darker Than Black", "darkerthanblack")] + [InlineData("Darker Than Black - Something", "darkerthanblacksomething")] + [InlineData("Darker Than_Black", "darkerthanblack")] + [InlineData("", "")] + public void NormalizeTest(string input, string expected) + { + Assert.Equal(expected, Normalize(input)); + } + + + + [Theory] + [InlineData("test.jpg", true)] + [InlineData("test.jpeg", true)] + [InlineData("test.png", true)] + [InlineData(".test.jpg", false)] + [InlineData("!test.jpg", false)] + public void IsImageTest(string filename, bool expected) + { + Assert.Equal(expected, IsImage(filename)); + } + + [Theory] + [InlineData("C:/", "C:/Love Hina/Love Hina - Special.cbz", "Love Hina")] + [InlineData("C:/", "C:/Love Hina/Specials/Ani-Hina Art Collection.cbz", "Love Hina")] + [InlineData("C:/", "C:/Mujaki no Rakuen Something/Mujaki no Rakuen Vol12 ch76.cbz", "Mujaki no Rakuen")] + public void FallbackTest(string rootDir, string inputPath, string expectedSeries) + { + var actual = Parse(inputPath, rootDir); + if (actual == null) + { + Assert.NotNull(actual); + return; + } + + Assert.Equal(expectedSeries, actual.Series); + } + + [Theory] + [InlineData("Love Hina - Special.jpg", false)] + [InlineData("folder.jpg", true)] + [InlineData("DearS_v01_cover.jpg", true)] + [InlineData("DearS_v01_covers.jpg", false)] + [InlineData("!cover.jpg", true)] + [InlineData("cover.jpg", true)] + [InlineData("cover.png", true)] + [InlineData("ch1/cover.png", true)] + public void IsCoverImageTest(string inputPath, bool expected) + { + Assert.Equal(expected, IsCoverImage(inputPath)); + } + + [Theory] + [InlineData("__MACOSX/Love Hina - Special.jpg", true)] + [InlineData("TEST/Love Hina - Special.jpg", false)] + [InlineData("__macosx/Love Hina/", false)] + [InlineData("MACOSX/Love Hina/", false)] + public void HasBlacklistedFolderInPathTest(string inputPath, bool expected) + { + Assert.Equal(expected, HasBlacklistedFolderInPath(inputPath)); + } + } +} \ No newline at end of file diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 8cfa382f2..d907ab75a 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,9 +1,7 @@ -using System.Collections.ObjectModel; -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.IO.Compression; using API.Archive; -using API.Interfaces.Services; using API.Services; using Microsoft.Extensions.Logging; using NSubstitute; diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs deleted file mode 100644 index 878b57c94..000000000 --- a/API.Tests/Services/BackupServiceTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using API.Interfaces; -using API.Services; -using API.Services.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using NSubstitute; - -namespace API.Tests.Services -{ - public class BackupServiceTests - { - private readonly DirectoryService _directoryService; - private readonly BackupService _backupService; - private readonly IUnitOfWork _unitOfWork = Substitute.For(); - private readonly ILogger _directoryLogger = Substitute.For>(); - private readonly ILogger _logger = Substitute.For>(); - private readonly IConfiguration _config; - - // public BackupServiceTests() - // { - // var inMemorySettings = new Dictionary { - // {"Logging:File:MaxRollingFiles", "0"}, - // {"Logging:File:Path", "file.log"}, - // }; - // - // _config = new ConfigurationBuilder() - // .AddInMemoryCollection(inMemorySettings) - // .Build(); - // - // //_config.GetMaxRollingFiles().Returns(0); - // //_config.GetLoggingFileName().Returns("file.log"); - // //var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BackupService/"); - // //Directory.GetCurrentDirectory().Returns(testDirectory); - // - // _directoryService = new DirectoryService(_directoryLogger); - // _backupService = new BackupService(_unitOfWork, _logger, _directoryService, _config); - // } - // - // [Fact] - // public void Test() - // { - // _backupService.BackupDatabase(); - // } - - - } -} \ No newline at end of file diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs new file mode 100644 index 000000000..1a53af8fc --- /dev/null +++ b/API.Tests/Services/BookServiceTests.cs @@ -0,0 +1,32 @@ +using System.IO; +using API.Entities.Interfaces; +using API.Interfaces; +using API.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace API.Tests.Services +{ + public class BookServiceTests + { + private readonly IBookService _bookService; + private readonly ILogger _logger = Substitute.For>(); + + public BookServiceTests() + { + _bookService = new BookService(_logger); + } + + [Theory] + [InlineData("The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub", 16)] + [InlineData("Non-existent file.epub", 0)] + [InlineData("Non an ebub.pdf", 0)] + public void GetNumberOfPagesTest(string filePath, int expectedPages) + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService/EPUB"); + Assert.Equal(expectedPages, _bookService.GetNumberOfPages(Path.Join(testDirectory, filePath))); + } + + } +} \ No newline at end of file diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 2072dae1f..410c43ade 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -41,7 +41,7 @@ // //[InlineData("", 0, "")] // public void GetCachedPagePathTest_Should() // { - // // TODO: Figure out how to test this + // // // string archivePath = "flat file.zip"; // // int pageNum = 0; // // string expected = "cache/1/pexels-photo-6551949.jpg"; diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 7c21ae927..04108fd25 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -1,6 +1,8 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Linq; using API.Services; +using API.Tests.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -18,6 +20,18 @@ namespace API.Tests.Services _directoryService = new DirectoryService(_logger); } + [Theory] + [InlineData("Manga-testcase.txt", 28)] + public void GetFilesTest(string file, int expectedFileCount) + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Manga"); + var files = new List(); + var fileCount = DirectoryService.TraverseTreeParallelForEach(testDirectory, s => files.Add(s), + API.Parser.Parser.ArchiveFileExtensions, _logger); + + Assert.Equal(expectedFileCount, fileCount); + } + [Fact] public void GetFiles_WithCustomRegex_ShouldPass_Test() { diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index bc1ea6de0..22f54259c 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -1,69 +1,102 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Data.Common; +using System.IO; using System.Linq; +using System.Threading.Tasks; +using API.Data; using API.Entities; -using API.Extensions; using API.Interfaces; using API.Interfaces.Services; using API.Parser; using API.Services; using API.Services.Tasks; +using API.Tests.Helpers; +using AutoMapper; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; -using NSubstitute.Extensions; using Xunit; using Xunit.Abstractions; namespace API.Tests.Services { - public class ScannerServiceTests + public class ScannerServiceTests : IDisposable { private readonly ITestOutputHelper _testOutputHelper; private readonly ScannerService _scannerService; private readonly ILogger _logger = Substitute.For>(); - private readonly IUnitOfWork _unitOfWork = Substitute.For(); + private readonly IUnitOfWork _unitOfWork; private readonly IArchiveService _archiveService = Substitute.For(); + private readonly IBookService _bookService = Substitute.For(); private readonly IMetadataService _metadataService; private readonly ILogger _metadataLogger = Substitute.For>(); - private Library _libraryMock; + + private readonly DbConnection _connection; + private readonly DataContext _context; + public ScannerServiceTests(ITestOutputHelper testOutputHelper) { - _testOutputHelper = testOutputHelper; - _scannerService = new ScannerService(_unitOfWork, _logger, _archiveService, _metadataService); - _metadataService= Substitute.For(_unitOfWork, _metadataLogger, _archiveService); - // _libraryMock = new Library() - // { - // Id = 1, - // Name = "Manga", - // Folders = new List() - // { - // new FolderPath() - // { - // Id = 1, - // LastScanned = DateTime.Now, - // LibraryId = 1, - // Path = "E:/Manga" - // } - // }, - // LastModified = DateTime.Now, - // Series = new List() - // { - // new Series() - // { - // Id = 0, - // Name = "Darker Than Black" - // } - // } - // }; + var contextOptions = new DbContextOptionsBuilder() + .UseSqlite(CreateInMemoryDatabase()) + .Options; + _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; + + _context = new DataContext(contextOptions); + Task.Run(SeedDb).GetAwaiter().GetResult(); + + //BackgroundJob.Enqueue is what I need to mock or something (it's static...) + // ICacheService cacheService, ILogger logger, IScannerService scannerService, + // IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService, + // IBackgroundJobClient jobClient + //var taskScheduler = new TaskScheduler(Substitute.For(), Substitute.For>(), Substitute.For<) + + + // Substitute.For>() - Not needed because only for UserService + _unitOfWork = new UnitOfWork(_context, Substitute.For(), null, + Substitute.For>()); + + + _testOutputHelper = testOutputHelper; + _metadataService= Substitute.For(_unitOfWork, _metadataLogger, _archiveService, _bookService); + _scannerService = new ScannerService(_unitOfWork, _logger, _archiveService, _metadataService, _bookService); } + private async Task SeedDb() + { + await _context.Database.MigrateAsync(); + await Seed.SeedSettings(_context); + + _context.Library.Add(new Library() + { + Name = "Manga", + Folders = new List() + { + new FolderPath() + { + Path = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Manga") + } + } + }); + return await _context.SaveChangesAsync() > 0; + } + + // [Fact] + // public void Test() + // { + // _scannerService.ScanLibrary(1, false); + // + // var series = _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1).Result.Series; + // } + [Fact] public void FindSeriesNotOnDisk_Should_RemoveNothing_Test() { - var scannerService = new ScannerService(_unitOfWork, _logger, _archiveService, _metadataService); var infos = new Dictionary>(); AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black"}); @@ -76,38 +109,36 @@ namespace API.Tests.Services Name = "Cage of Eden", LocalizedName = "Cage of Eden", OriginalName = "Cage of Eden", - NormalizedName = Parser.Parser.Normalize("Cage of Eden") + NormalizedName = API.Parser.Parser.Normalize("Cage of Eden") }); existingSeries.Add(new Series() { Name = "Darker Than Black", LocalizedName = "Darker Than Black", OriginalName = "Darker Than Black", - NormalizedName = Parser.Parser.Normalize("Darker Than Black") + NormalizedName = API.Parser.Parser.Normalize("Darker Than Black") }); var expectedSeries = new List(); - Assert.Empty(scannerService.FindSeriesNotOnDisk(existingSeries, infos)); + Assert.Empty(_scannerService.FindSeriesNotOnDisk(existingSeries, infos)); } [Theory] [InlineData(new [] {"Darker than Black"}, "Darker than Black", "Darker than Black")] [InlineData(new [] {"Darker than Black"}, "Darker Than Black", "Darker than Black")] - [InlineData(new [] {"Darker than Black"}, "Darker Than Black!", "Darker Than Black!")] + [InlineData(new [] {"Darker than Black"}, "Darker Than Black!", "Darker than Black")] [InlineData(new [] {""}, "Runaway Jack", "Runaway Jack")] public void MergeNameTest(string[] existingSeriesNames, string parsedInfoName, string expected) { - var scannerService = new ScannerService(_unitOfWork, _logger, _archiveService, _metadataService); - var collectedSeries = new ConcurrentDictionary>(); foreach (var seriesName in existingSeriesNames) { AddToParsedInfo(collectedSeries, new ParserInfo() {Series = seriesName}); } - var actualName = scannerService.MergeName(collectedSeries, new ParserInfo() + var actualName = _scannerService.MergeName(collectedSeries, new ParserInfo() { Series = parsedInfoName }); @@ -115,6 +146,25 @@ namespace API.Tests.Services Assert.Equal(expected, actualName); } + [Fact] + public void RemoveMissingSeries_Should_RemoveSeries() + { + var existingSeries = new List() + { + EntityFactory.CreateSeries("Darker than Black Vol 1"), + EntityFactory.CreateSeries("Darker than Black"), + EntityFactory.CreateSeries("Beastars"), + }; + var missingSeries = new List() + { + EntityFactory.CreateSeries("Darker than Black Vol 1"), + }; + existingSeries = ScannerService.RemoveMissingSeries(existingSeries, missingSeries, out var removeCount).ToList(); + + Assert.DoesNotContain(missingSeries[0].Name, existingSeries.Select(s => s.Name)); + Assert.Equal(missingSeries.Count, removeCount); + } + private void AddToParsedInfo(IDictionary> collectedSeries, ParserInfo info) { if (collectedSeries.GetType() == typeof(ConcurrentDictionary<,>)) @@ -209,5 +259,16 @@ namespace API.Tests.Services // _testOutputHelper.WriteLine(_libraryMock.ToString()); Assert.True(true); } + + private static DbConnection CreateInMemoryDatabase() + { + var connection = new SqliteConnection("Filename=:memory:"); + + connection.Open(); + + return connection; + } + + public void Dispose() => _connection.Dispose(); } } \ No newline at end of file diff --git a/API.Tests/Services/Test Data/BookService/EPUB/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub b/API.Tests/Services/Test Data/BookService/EPUB/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub new file mode 100644 index 0000000000000000000000000000000000000000..7388bc85e35b439b07347ebe079d61972a32fbab GIT binary patch literal 258623 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

?6fQNPmY8Wb}zKz1N}_W;wsZKbwyYR9O@& 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

?~l~udA;0|0#7vw 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 net5.0 true + Linux @@ -12,10 +13,12 @@ + + @@ -36,6 +39,7 @@ + diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs new file mode 100644 index 000000000..b7589e6dd --- /dev/null +++ b/API/Controllers/BookController.cs @@ -0,0 +1,220 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities.Interfaces; +using API.Extensions; +using API.Interfaces; +using API.Services; +using HtmlAgilityPack; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using VersOne.Epub; + +namespace API.Controllers +{ + public class BookController : BaseApiController + { + private readonly ILogger _logger; + private readonly IBookService _bookService; + private readonly IUnitOfWork _unitOfWork; + private static readonly string BookApiUrl = "book-resources?file="; + + + public BookController(ILogger logger, IBookService bookService, IUnitOfWork unitOfWork) + { + _logger = logger; + _bookService = bookService; + _unitOfWork = unitOfWork; + } + + [HttpGet("{chapterId}/book-info")] + public async Task> GetBookInfo(int chapterId) + { + var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId); + var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath); + + return book.Title; + } + + [HttpGet("{chapterId}/book-resources")] + public async Task GetBookPageResources(int chapterId, [FromQuery] string file) + { + var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId); + var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath); + + var key = BookService.CleanContentKeys(file); + if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book"); + + var bookFile = book.Content.AllFiles[key]; + var content = await bookFile.ReadContentAsBytesAsync(); + Response.AddCacheHeader(content); + var contentType = BookService.GetContentType(bookFile.ContentType); + return File(content, contentType, $"{chapterId}-{file}"); + } + + [HttpGet("{chapterId}/chapters")] + public async Task>> GetBookChapters(int chapterId) + { + // This will return a list of mappings from ID -> pagenum. ID will be the xhtml key and pagenum will be the reading order + // this is used to rewrite anchors in the book text so that we always load properly in FE + var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId); + var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath); + var mappings = await _bookService.CreateKeyToPageMappingAsync(book); + + var navItems = await book.GetNavigationAsync(); + var chaptersList = new List(); + + foreach (var navigationItem in navItems) + { + if (navigationItem.NestedItems.Count > 0) + { + _logger.LogDebug("Header: {Header}", navigationItem.Title); + var nestedChapters = new List(); + + foreach (var nestedChapter in navigationItem.NestedItems) + { + if (nestedChapter.Link == null) continue; + var key = BookService.CleanContentKeys(nestedChapter.Link.ContentFileName); + if (mappings.ContainsKey(key)) + { + nestedChapters.Add(new BookChapterItem() + { + Title = nestedChapter.Title, + Page = mappings[key], + Part = nestedChapter.Link.Anchor ?? string.Empty, + Children = new List() + }); + } + } + + if (navigationItem.Link == null) + { + var item = new BookChapterItem() + { + Title = navigationItem.Title, + Children = nestedChapters + }; + if (nestedChapters.Count > 0) + { + item.Page = nestedChapters[0].Page; + } + chaptersList.Add(item); + } + else + { + var groupKey = BookService.CleanContentKeys(navigationItem.Link.ContentFileName); + if (mappings.ContainsKey(groupKey)) + { + chaptersList.Add(new BookChapterItem() + { + Title = navigationItem.Title, + Page = mappings[groupKey], + Children = nestedChapters + }); + } + } + } + } + return Ok(chaptersList); + } + + [HttpGet("{chapterId}/book-page")] + public async Task> GetBookPage(int chapterId, [FromQuery] int page) + { + var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId); + + var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath); + var mappings = await _bookService.CreateKeyToPageMappingAsync(book); + + var counter = 0; + var doc = new HtmlDocument(); + var baseUrl = Request.Scheme + "://" + Request.Host + Request.PathBase + "/api/"; + var apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl; + var bookPages = await book.GetReadingOrderAsync(); + foreach (var contentFileRef in bookPages) + { + if (page == counter) + { + var content = await contentFileRef.ReadContentAsync(); + if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return Ok(content); + + doc.LoadHtml(content); + var body = doc.DocumentNode.SelectSingleNode("/html/body"); + + var inlineStyles = doc.DocumentNode.SelectNodes("//style"); + if (inlineStyles != null) + { + foreach (var inlineStyle in inlineStyles) + { + var styleContent = await _bookService.ScopeStyles(inlineStyle.InnerHtml, apiBase); + body.PrependChild(HtmlNode.CreateNode($"")); + } + } + + var styleNodes = doc.DocumentNode.SelectNodes("/html/head/link"); + if (styleNodes != null) + { + foreach (var styleLinks in styleNodes) + { + var key = BookService.CleanContentKeys(styleLinks.Attributes["href"].Value); + var styleContent = await _bookService.ScopeStyles(await book.Content.Css[key].ReadContentAsync(), apiBase); + body.PrependChild(HtmlNode.CreateNode($"")); + } + } + + var anchors = doc.DocumentNode.SelectNodes("//a"); + if (anchors != null) + { + foreach (var anchor in anchors) + { + BookService.UpdateLinks(anchor, mappings, page); + } + } + + var images = doc.DocumentNode.SelectNodes("//img"); + if (images != null) + { + foreach (var image in images) + { + if (image.Name != "img") continue; + + // Need to do for xlink:href + if (image.Attributes["src"] != null) + { + var imageFile = image.Attributes["src"].Value; + image.Attributes.Remove("src"); + image.Attributes.Add("src", $"{apiBase}" + imageFile); + } + } + } + + images = doc.DocumentNode.SelectNodes("//image"); + if (images != null) + { + foreach (var image in images) + { + if (image.Name != "image") continue; + + if (image.Attributes["xlink:href"] != null) + { + var imageFile = image.Attributes["xlink:href"].Value; + image.Attributes.Remove("xlink:href"); + image.Attributes.Add("xlink:href", $"{apiBase}" + imageFile); + } + } + } + + + + + return Ok(body.InnerHtml); + } + + counter++; + } + + return BadRequest("Could not find the appropriate html for that page"); + } + } +} \ No newline at end of file diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index c1b6df2b8..4867be3d8 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.Entities; +using API.Entities.Enums; using API.Extensions; -using API.Helpers; using API.Interfaces; using API.Interfaces.Services; using AutoMapper; @@ -223,5 +223,11 @@ namespace API.Controllers return Ok(series); } + + [HttpGet("type")] + public async Task> GetLibraryType(int libraryId) + { + return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId)); + } } } \ No newline at end of file diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 491b08a17..e3927146b 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -46,7 +46,7 @@ namespace API.Controllers return File(content, "image/" + format); } - + [HttpGet("chapter-path")] public async Task> GetImagePath(int chapterId) { diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index d149aa0d4..33565af56 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -105,6 +105,13 @@ namespace API.Controllers return Ok(CronConverter.Options); } + [Authorize(Policy = "RequireAdminRole")] + [HttpGet("library-types")] + public ActionResult> GetLibraryTypes() + { + return Ok(Enum.GetNames(typeof(LibraryType))); + } + [Authorize(Policy = "RequireAdminRole")] [HttpGet("log-levels")] public ActionResult> GetLogLevels() diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 607f508e9..b51706217 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -38,6 +38,14 @@ namespace API.Controllers return Ok(await _unitOfWork.UserRepository.GetMembersAsync()); } + [HttpGet("has-reading-progress")] + public async Task> HasReadingProgress(int libraryId) + { + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, user.Id)); + } + [HttpGet("has-library-access")] public async Task> HasLibraryAccess(int libraryId) { @@ -53,7 +61,11 @@ namespace API.Controllers existingPreferences.ReadingDirection = preferencesDto.ReadingDirection; existingPreferences.ScalingOption = preferencesDto.ScalingOption; existingPreferences.PageSplitOption = preferencesDto.PageSplitOption; - existingPreferences.HideReadOnDetails = preferencesDto.HideReadOnDetails; + existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin; + existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing; + existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily; + existingPreferences.BookReaderDarkMode = preferencesDto.BookReaderDarkMode; + existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize; _unitOfWork.UserRepository.Update(existingPreferences); diff --git a/API/DTOs/BookChapterItem.cs b/API/DTOs/BookChapterItem.cs new file mode 100644 index 000000000..68d1fce40 --- /dev/null +++ b/API/DTOs/BookChapterItem.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace API.DTOs +{ + public class BookChapterItem + { + ///

+ /// Name of the Chapter + /// + public string Title { get; set; } + /// + /// A part represents the id of the anchor so we can scroll to it. 01_values.xhtml#h_sVZPaxUSy/ + /// + public string Part { get; set; } + /// + /// Page Number to load for the chapter + /// + public int Page { get; set; } + public ICollection Children { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index 66934d040..4dcabee33 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -22,6 +22,10 @@ namespace API.DTOs /// public bool IsSpecial { get; init; } /// + /// Used for books/specials to display custom title. For non-specials/books, will be set to + /// + public string Title { get; init; } + /// /// The files that represent this Chapter /// public ICollection Files { get; init; } diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index bec209a5b..ea2a563e2 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -7,9 +7,10 @@ namespace API.DTOs public ReadingDirection ReadingDirection { get; set; } public ScalingOption ScalingOption { get; set; } public PageSplitOption PageSplitOption { get; set; } - /// - /// Whether UI hides read Volumes on Details page - /// - public bool HideReadOnDetails { get; set; } + public bool BookReaderDarkMode { get; set; } = false; + public int BookReaderMargin { get; set; } + public int BookReaderLineSpacing { get; set; } + public int BookReaderFontSize { get; set; } + public string BookReaderFontFamily { get; set; } } } \ No newline at end of file diff --git a/API/Data/AppUserProgressRepository.cs b/API/Data/AppUserProgressRepository.cs index a65ab2c48..38912b589 100644 --- a/API/Data/AppUserProgressRepository.cs +++ b/API/Data/AppUserProgressRepository.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using API.Entities.Enums; using API.Interfaces; using Microsoft.EntityFrameworkCore; @@ -28,5 +29,28 @@ namespace API.Data _context.RemoveRange(rowsToRemove); return await _context.SaveChangesAsync() > 0 ? rowsToRemove.Count : 0; } + + /// + /// Checks if user has any progress against a library of passed type + /// + /// + /// + /// + public async Task UserHasProgress(LibraryType libraryType, int userId) + { + var seriesIds = await _context.AppUserProgresses + .Where(aup => aup.PagesRead > 0 && aup.AppUserId == userId) + .AsNoTracking() + .Select(aup => aup.SeriesId) + .ToListAsync(); + + if (seriesIds.Count == 0) return false; + + return await _context.Series + .Include(s => s.Library) + .Where(s => seriesIds.Contains(s.Id) && s.Library.Type == libraryType) + .AsNoTracking() + .AnyAsync(); + } } } \ No newline at end of file diff --git a/API/Data/DbFactory.cs b/API/Data/DbFactory.cs new file mode 100644 index 000000000..3589fc30e --- /dev/null +++ b/API/Data/DbFactory.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using API.Entities; +using API.Entities.Enums; +using API.Parser; +using API.Services.Tasks; + +namespace API.Data +{ + /// + /// Responsible for creating Series, Volume, Chapter, MangaFiles for use in + /// + public static class DbFactory + { + public static Series Series(string name) + { + return new () + { + Name = name, + OriginalName = name, + LocalizedName = name, + NormalizedName = Parser.Parser.Normalize(name), + SortName = name, + Summary = string.Empty, + Volumes = new List() + }; + } + + public static Volume Volume(string volumeNumber) + { + return new Volume() + { + Name = volumeNumber, + Number = (int) Parser.Parser.MinimumNumberFromRange(volumeNumber), + Chapters = new List() + }; + } + + public static Chapter Chapter(ParserInfo info) + { + var specialTreatment = info.IsSpecialInfo(); + var specialTitle = specialTreatment ? info.Filename : info.Chapters; + return new Chapter() + { + Number = specialTreatment ? "0" : Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty, + Range = specialTreatment ? info.Filename : info.Chapters, + Title = (specialTreatment && info.Format == MangaFormat.Book) + ? info.Title + : specialTitle, + Files = new List(), + IsSpecial = specialTreatment, + }; + } + } +} \ No newline at end of file diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index 707e7c62c..c065bface 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.Entities; +using API.Entities.Enums; using API.Interfaces; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -68,6 +67,15 @@ namespace API.Data .ToListAsync(); } + public async Task GetLibraryTypeAsync(int libraryId) + { + return await _context.Library + .Where(l => l.Id == libraryId) + .AsNoTracking() + .Select(l => l.Type) + .SingleAsync(); + } + public async Task> GetLibraryDtosAsync() { return await _context.Library diff --git a/API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs b/API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs new file mode 100644 index 000000000..eb4dd459a --- /dev/null +++ b/API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs @@ -0,0 +1,748 @@ +// +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("20210419222000_BookReaderPreferences")] + partial class BookReaderPreferences + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.4"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("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.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookReaderDarkMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId") + .IsUnique(); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", 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("UserPreferences"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210419222000_BookReaderPreferences.cs b/API/Data/Migrations/20210419222000_BookReaderPreferences.cs new file mode 100644 index 000000000..0dd1089eb --- /dev/null +++ b/API/Data/Migrations/20210419222000_BookReaderPreferences.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class BookReaderPreferences : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "HideReadOnDetails", + table: "AppUserPreferences", + newName: "BookReaderMargin"); + + migrationBuilder.AddColumn( + name: "BookReaderDarkMode", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "BookReaderFontFamily", + table: "AppUserPreferences", + type: "TEXT", + nullable: true, + defaultValue: "default"); + + migrationBuilder.AddColumn( + name: "BookReaderLineSpacing", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: 100); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BookReaderDarkMode", + table: "AppUserPreferences"); + + migrationBuilder.DropColumn( + name: "BookReaderFontFamily", + table: "AppUserPreferences"); + + migrationBuilder.DropColumn( + name: "BookReaderLineSpacing", + table: "AppUserPreferences"); + + migrationBuilder.RenameColumn( + name: "BookReaderMargin", + table: "AppUserPreferences", + newName: "HideReadOnDetails"); + } + } +} diff --git a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs b/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs new file mode 100644 index 000000000..95005cf47 --- /dev/null +++ b/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs @@ -0,0 +1,751 @@ +// +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("20210419234652_BookReaderPreferencesFontSize")] + partial class BookReaderPreferencesFontSize + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.4"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("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.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookReaderDarkMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId") + .IsUnique(); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", 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("UserPreferences"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs b/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs new file mode 100644 index 000000000..1745e4f73 --- /dev/null +++ b/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class BookReaderPreferencesFontSize : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BookReaderFontSize", + table: "AppUserPreferences", + type: "INTEGER", + nullable: false, + defaultValue: 100); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BookReaderFontSize", + table: "AppUserPreferences"); + } + } +} diff --git a/API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs b/API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs new file mode 100644 index 000000000..693480dd3 --- /dev/null +++ b/API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs @@ -0,0 +1,751 @@ +// +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("20210423132900_CustomChapterTitle")] + partial class CustomChapterTitle + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.4"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("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.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookReaderDarkMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId") + .IsUnique(); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", 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("UserPreferences"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210423132900_CustomChapterTitle.cs b/API/Data/Migrations/20210423132900_CustomChapterTitle.cs new file mode 100644 index 000000000..b3958127c --- /dev/null +++ b/API/Data/Migrations/20210423132900_CustomChapterTitle.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class CustomChapterTitle : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsSpecial", + table: "Volume"); + + migrationBuilder.AddColumn( + name: "Title", + table: "Chapter", + type: "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Title", + table: "Chapter"); + + migrationBuilder.AddColumn( + name: "IsSpecial", + table: "Volume", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 09fe5689f..abcb68281 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace API.Data.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.1"); + .HasAnnotation("ProductVersion", "5.0.4"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -127,7 +127,19 @@ namespace API.Data.Migrations b.Property("AppUserId") .HasColumnType("INTEGER"); - b.Property("HideReadOnDetails") + b.Property("BookReaderDarkMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") .HasColumnType("INTEGER"); b.Property("PageSplitOption") @@ -248,6 +260,9 @@ namespace API.Data.Migrations b.Property("Range") .HasColumnType("TEXT"); + b.Property("Title") + .HasColumnType("TEXT"); + b.Property("VolumeId") .HasColumnType("INTEGER"); @@ -412,9 +427,6 @@ namespace API.Data.Migrations b.Property("Created") .HasColumnType("TEXT"); - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - b.Property("LastModified") .HasColumnType("TEXT"); diff --git a/API/Dockerfile b/API/Dockerfile new file mode 100644 index 000000000..d813139f8 --- /dev/null +++ b/API/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +WORKDIR /src +COPY ["API/API.csproj", "API/"] +RUN dotnet restore "API/API.csproj" +COPY . . +WORKDIR "/src/API" +RUN dotnet build "API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "API.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "API.dll"] diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index 1a2e6b41b..a4a773a38 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -5,13 +5,39 @@ namespace API.Entities public class AppUserPreferences { public int Id { get; set; } - public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight; - public ScalingOption ScalingOption { get; set; } = ScalingOption.FitToHeight; - public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.SplitRightToLeft; /// - /// Whether UI hides read Volumes on Details page + /// Manga Reader Option: What direction should the next/prev page buttons go /// - public bool HideReadOnDetails { get; set; } = false; + public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight; + /// + /// Manga Reader Option: How should the image be scaled to screen + /// + public ScalingOption ScalingOption { get; set; } = ScalingOption.FitToHeight; + /// + /// Manga Reader Option: Which side of a split image should we show first + /// + public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.SplitRightToLeft; + + /// + /// Book Reader Option: Should the background color be dark + /// + public bool BookReaderDarkMode { get; set; } = false; + /// + /// Book Reader Option: Override extra Margin + /// + public int BookReaderMargin { get; set; } = 15; + /// + /// Book Reader Option: Override line-height + /// + public int BookReaderLineSpacing { get; set; } = 100; + /// + /// Book Reader Option: Override font size + /// + public int BookReaderFontSize { get; set; } = 100; + /// + /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override + /// + public string BookReaderFontFamily { get; set; } = "default"; diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index 015c4e4d8..31f4dc513 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using API.Entities.Enums; using API.Entities.Interfaces; +using API.Parser; namespace API.Entities { @@ -30,10 +32,27 @@ namespace API.Entities /// If this Chapter contains files that could only be identified as Series or has Special Identifier from filename /// public bool IsSpecial { get; set; } + /// + /// Used for books/specials to display custom title. For non-specials/books, will be set to + /// + public string Title { get; set; } // Relationships public Volume Volume { get; set; } public int VolumeId { get; set; } + public void UpdateFrom(ParserInfo info) + { + Files ??= new List(); + IsSpecial = info.IsSpecialInfo(); + if (IsSpecial) + { + Number = "0"; + } + Title = (IsSpecial && info.Format == MangaFormat.Book) + ? info.Title + : Range; + + } } } \ No newline at end of file diff --git a/API/Entities/Enums/MangaFormat.cs b/API/Entities/Enums/MangaFormat.cs index 31ebd5bb3..121aa3e1c 100644 --- a/API/Entities/Enums/MangaFormat.cs +++ b/API/Entities/Enums/MangaFormat.cs @@ -9,6 +9,8 @@ namespace API.Entities.Enums [Description("Archive")] Archive = 1, [Description("Unknown")] - Unknown = 2 + Unknown = 2, + [Description("Book")] + Book = 3 } } \ No newline at end of file diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index ddf2ea3fc..2efb76bfb 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -1,6 +1,8 @@  using System; +using System.IO; using API.Entities.Enums; +using API.Extensions; namespace API.Entities { @@ -24,5 +26,11 @@ namespace API.Entities // Relationship Mapping public Chapter Chapter { get; set; } public int ChapterId { get; set; } + + // Methods + public bool HasFileBeenModified() + { + return new FileInfo(FilePath).DoesLastWriteMatch(LastModified); + } } } \ No newline at end of file diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 6406e118f..0ad7c8c16 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -45,5 +45,6 @@ namespace API.Entities public List Volumes { get; set; } public Library Library { get; set; } public int LibraryId { get; set; } + } } \ No newline at end of file diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 999b9a801..dab9f2e1b 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -15,12 +15,7 @@ namespace API.Entities public byte[] CoverImage { get; set; } public int Pages { get; set; } - /// - /// Represents a Side story that is linked to the original Series. Omake, One Shot, etc. - /// - public bool IsSpecial { get; set; } = false; - - + // Relationships public Series Series { get; set; } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 89b338e5c..b0e09e18f 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -1,4 +1,5 @@ using API.Data; +using API.Entities.Interfaces; using API.Helpers; using API.Interfaces; using API.Interfaces.Services; @@ -26,8 +27,8 @@ namespace API.Extensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - - + services.AddScoped(); + services.AddDbContext(options => { diff --git a/API/Extensions/ChapterListExtensions.cs b/API/Extensions/ChapterListExtensions.cs new file mode 100644 index 000000000..6362c0571 --- /dev/null +++ b/API/Extensions/ChapterListExtensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using API.Entities; +using API.Parser; + +namespace API.Extensions +{ + public static class ChapterListExtensions + { + /// + /// Returns first chapter in the list with at least one file + /// + /// + /// + public static Chapter GetFirstChapterWithFiles(this IList chapters) + { + return chapters.FirstOrDefault(c => c.Files.Any()); + } + + /// + /// Gets a single chapter (or null if doesn't exist) where Range matches the info.Chapters property. If the info + /// is then, the filename is used to search against Range or if filename exists within Files of said Chapter. + /// + /// + /// + /// + public static Chapter GetChapterByRange(this IList chapters, ParserInfo info) + { + var specialTreatment = info.IsSpecialInfo(); + return specialTreatment + ? chapters.SingleOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath))) + : chapters.SingleOrDefault(c => c.Range == info.Chapters); + } + } +} \ No newline at end of file diff --git a/API/Extensions/ParserInfoListExtensions.cs b/API/Extensions/ParserInfoListExtensions.cs new file mode 100644 index 000000000..adbf32c3c --- /dev/null +++ b/API/Extensions/ParserInfoListExtensions.cs @@ -0,0 +1,34 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using API.Entities; +using API.Parser; + +namespace API.Extensions +{ + public static class ParserInfoListExtensions + { + /// + /// Selects distinct volume numbers by the "Volumes" key on the ParserInfo + /// + /// + /// + public static IList DistinctVolumes(this IList infos) + { + return infos.Select(p => p.Volumes).Distinct().ToList(); + } + + /// + /// Checks if a list of ParserInfos has a given chapter or not. Lookup occurs on Range property. If a chapter is + /// special, then the is matched, else the field is checked. + /// + /// + /// + /// + public static bool HasInfo(this IList infos, Chapter chapter) + { + return chapter.IsSpecial ? infos.Any(v => v.Filename == chapter.Range) + : infos.Any(v => v.Chapters == chapter.Range); + } + } +} \ No newline at end of file diff --git a/API/Extensions/SeriesExtensions.cs b/API/Extensions/SeriesExtensions.cs index 5680c52d2..29f495d76 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/API/Extensions/SeriesExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using API.Entities; namespace API.Extensions @@ -13,15 +14,7 @@ namespace API.Extensions /// public static bool NameInList(this Series series, IEnumerable list) { - foreach (var name in list) - { - if (Parser.Parser.Normalize(name) == series.NormalizedName || name == series.Name || name == series.LocalizedName || name == series.OriginalName) - { - return true; - } - } - - return false; + return list.Any(name => Parser.Parser.Normalize(name) == series.NormalizedName || Parser.Parser.Normalize(name) == Parser.Parser.Normalize(series.Name) || name == series.Name || name == series.LocalizedName || name == series.OriginalName); } } } \ No newline at end of file diff --git a/API/Extensions/VolumeListExtensions.cs b/API/Extensions/VolumeListExtensions.cs new file mode 100644 index 000000000..5b50d382f --- /dev/null +++ b/API/Extensions/VolumeListExtensions.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using API.Entities; +using API.Entities.Enums; + +namespace API.Extensions +{ + public static class VolumeListExtensions + { + public static Volume FirstWithChapters(this IList volumes, bool inBookSeries) + { + return inBookSeries + ? volumes.FirstOrDefault(v => v.Chapters.Any()) + : volumes.FirstOrDefault(v => v.Chapters.Any() && (v.Number == 1)); + } + + /// + /// Selects the first Volume to get the cover image from. For a book with only a special, the special will be returned. + /// If there are both specials and non-specials, then the first non-special will be returned. + /// + /// + /// + /// + public static Volume GetCoverImage(this IList volumes, LibraryType libraryType) + { + if (libraryType == LibraryType.Book) + { + return volumes.OrderBy(x => x.Number).FirstOrDefault(); + } + + if (volumes.Any(x => x.Number != 0)) + { + return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0); + } + return volumes.OrderBy(x => x.Number).FirstOrDefault(); + } + } +} \ No newline at end of file diff --git a/API/Helpers/Converters/CronConverter.cs b/API/Helpers/Converters/CronConverter.cs index 6fece1bdb..cacf018b1 100644 --- a/API/Helpers/Converters/CronConverter.cs +++ b/API/Helpers/Converters/CronConverter.cs @@ -13,7 +13,7 @@ namespace API.Helpers.Converters }; public static string ConvertToCronNotation(string source) { - string destination = ""; + var destination = string.Empty; destination = source.ToLower() switch { "daily" => Cron.Daily(), @@ -28,7 +28,7 @@ namespace API.Helpers.Converters public static string ConvertFromCronNotation(string cronNotation) { - string destination = ""; + var destination = string.Empty; destination = cronNotation.ToLower() switch { "0 0 31 2 *" => "disabled", diff --git a/API/Interfaces/IAppUserProgressRepository.cs b/API/Interfaces/IAppUserProgressRepository.cs index a268ac5f5..96ada0c50 100644 --- a/API/Interfaces/IAppUserProgressRepository.cs +++ b/API/Interfaces/IAppUserProgressRepository.cs @@ -1,9 +1,11 @@ using System.Threading.Tasks; +using API.Entities.Enums; namespace API.Interfaces { public interface IAppUserProgressRepository { Task CleanupAbandonedChapters(); + Task UserHasProgress(LibraryType libraryType, int userId); } } \ No newline at end of file diff --git a/API/Interfaces/IBookService.cs b/API/Interfaces/IBookService.cs new file mode 100644 index 000000000..43c3cd479 --- /dev/null +++ b/API/Interfaces/IBookService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using VersOne.Epub; + +namespace API.Interfaces +{ + public interface IBookService + { + int GetNumberOfPages(string filePath); + byte[] GetCoverImage(string fileFilePath, bool createThumbnail = true); + Task> CreateKeyToPageMappingAsync(EpubBookRef book); + /// + /// Scopes styles to .reading-section and replaces img src to the passed apiBase + /// + /// + /// + /// + Task ScopeStyles(string stylesheetHtml, string apiBase); + string GetSummaryInfo(string filePath); + } +} \ No newline at end of file diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 43e0db6e6..f8cedce90 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using API.DTOs; using API.Entities; +using API.Entities.Enums; namespace API.Interfaces { @@ -17,5 +18,6 @@ namespace API.Interfaces Task> GetLibrariesAsync(); Task DeleteLibrary(int libraryId); Task> GetLibrariesForUserIdAsync(int userId); + Task GetLibraryTypeAsync(int libraryId); } } \ No newline at end of file diff --git a/API/Middleware/BookRedirectMiddleware.cs b/API/Middleware/BookRedirectMiddleware.cs new file mode 100644 index 000000000..f2e805466 --- /dev/null +++ b/API/Middleware/BookRedirectMiddleware.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace API.Middleware +{ + public class BookRedirectMiddleware + { + private readonly ILogger _logger; + + public BookRedirectMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + _logger.LogDebug("BookRedirect Path: {Path}", context.Request.Path.ToString()); + await next.Invoke(context); + } + } +} \ No newline at end of file diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index dc2e701e2..fbf5717e0 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -9,14 +9,19 @@ namespace API.Parser { public static class Parser { - public static readonly string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|.tar.gz|.7zip"; + public static readonly string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip"; + public static readonly string BookFileExtensions = @"\.epub"; public static readonly string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg)"; + public static readonly Regex FontSrcUrlRegex = new Regex("(src:url\\(\"?'?)([a-z0-9/\\._]+)(\"?'?\\))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly string XmlRegexExtensions = @"\.xml"; private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex BookFileRegex = new Regex(BookFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CoverImageRegex = new Regex(@"(?.*)( |_)Vol\.?\d+", + RegexOptions.IgnoreCase | RegexOptions.Compiled), // Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip new Regex( @"(?.*)(\b|_)v(?\d+-?\d*)( |_)", @@ -126,10 +135,7 @@ namespace API.Parser new Regex( @"^(?!Vol)(?.*)( |_)Chapter( |_)(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar - new Regex( - @"^(?.*)( |_)Vol\.?\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Fullmetal Alchemist chapters 101-108.cbz new Regex( @"^(?!vol)(?.*)( |_)(chapters( |_)?)\d+-?\d*", @@ -238,21 +244,21 @@ namespace API.Parser private static readonly Regex[] ComicChapterRegex = new[] { - // 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS) - new Regex( - @"^(?\d+) (- |_)?(?.*(\d{4})?)( |_)(\(|\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - // 01 Spider-Man & Wolverine 01.cbr - new Regex( - @"^(?\d+) (?:- )?(?.*) (\d+)?", - RegexOptions.IgnoreCase | RegexOptions.Compiled), + // // 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS) + // new Regex( + // @"^(?\d+) (- |_)?(?.*(\d{4})?)( |_)(\(|\d+)", + // RegexOptions.IgnoreCase | RegexOptions.Compiled), + // // 01 Spider-Man & Wolverine 01.cbr + // new Regex( + // @"^(?\d+) (?:- )?(?.*) (\d+)?", // NOTE: WHy is this here without a capture group + // RegexOptions.IgnoreCase | RegexOptions.Compiled), // Batman & Wildcat (1 of 3) new Regex( @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( - @"^(?.*)(?: |_)v(?\d+)(?: |_)(c? ?)(?\d+)", + @"^(?.*)(?: |_)v(?\d+)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @@ -262,6 +268,10 @@ namespace API.Parser new Regex( @"^(?.*)(?: |_)#(?\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr + new Regex( + @"^(?.*)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-", + RegexOptions.IgnoreCase | RegexOptions.Compiled), }; private static readonly Regex[] ReleaseGroupRegex = new[] @@ -350,7 +360,7 @@ namespace API.Parser { // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. new Regex( - @"(?Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection)", + @"(?Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection|Side( |_)Stories)", RegexOptions.IgnoreCase | RegexOptions.Compiled), }; @@ -366,17 +376,34 @@ namespace API.Parser public static ParserInfo Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) { var fileName = Path.GetFileName(filePath); + ParserInfo ret; - var ret = new ParserInfo() + if (type == LibraryType.Book) { - Chapters = type == LibraryType.Manga ? ParseChapter(fileName) : ParseComicChapter(fileName), - Series = type == LibraryType.Manga ? ParseSeries(fileName) : ParseComicSeries(fileName), - Volumes = type == LibraryType.Manga ? ParseVolume(fileName) : ParseComicVolume(fileName), - Filename = fileName, - Format = ParseFormat(filePath), - FullFilePath = filePath - }; - + ret = new ParserInfo() + { + Chapters = ParseChapter(fileName) ?? ParseComicChapter(fileName), + Series = ParseSeries(fileName) ?? ParseComicSeries(fileName), + Volumes = ParseVolume(fileName) ?? ParseComicVolume(fileName), + Filename = fileName, + Format = ParseFormat(filePath), + FullFilePath = filePath + }; + } + else + { + ret = new ParserInfo() + { + Chapters = type == LibraryType.Manga ? ParseChapter(fileName) : ParseComicChapter(fileName), + Series = type == LibraryType.Manga ? ParseSeries(fileName) : ParseComicSeries(fileName), + Volumes = type == LibraryType.Manga ? ParseVolume(fileName) : ParseComicVolume(fileName), + Filename = fileName, + Format = ParseFormat(filePath), + Title = Path.GetFileNameWithoutExtension(fileName), + FullFilePath = filePath + }; + } + if (ret.Series == string.Empty) { // Try to parse information out of each folder all the way to rootPath @@ -412,6 +439,8 @@ namespace API.Parser } var isSpecial = ParseMangaSpecial(fileName); + // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that + // could cause a problem as Omake is a special term, but there is valid volume/chapter information. if (ret.Chapters == "0" && ret.Volumes == "0" && !string.IsNullOrEmpty(isSpecial)) { ret.IsSpecial = true; @@ -426,6 +455,7 @@ namespace API.Parser { if (IsArchive(filePath)) return MangaFormat.Archive; if (IsImage(filePath)) return MangaFormat.Image; + if (IsBook(filePath)) return MangaFormat.Book; return MangaFormat.Unknown; } @@ -520,7 +550,7 @@ namespace API.Parser return "0"; } - + public static string ParseComicVolume(string filename) { foreach (var regex in ComicVolumeRegex) @@ -735,6 +765,10 @@ namespace API.Parser { return ArchiveFileRegex.IsMatch(Path.GetExtension(filePath)); } + public static bool IsBook(string filePath) + { + return BookFileRegex.IsMatch(Path.GetExtension(filePath)); + } public static bool IsImage(string filePath, bool suppressExtraChecks = false) { @@ -749,13 +783,13 @@ namespace API.Parser public static float MinimumNumberFromRange(string range) { - var tokens = range.Split("-"); + var tokens = range.Replace("_", string.Empty).Split("-"); return tokens.Min(float.Parse); } public static string Normalize(string name) { - return name.ToLower().Replace("-", "").Replace(" ", "").Replace(":", "").Replace("_", ""); + return Regex.Replace(name.ToLower(), "[^a-zA-Z0-9]", string.Empty); } /// @@ -773,6 +807,10 @@ namespace API.Parser return path.Contains("__MACOSX"); } - + + public static bool IsEpub(string filePath) + { + return Path.GetExtension(filePath).ToLower() == ".epub"; + } } } \ No newline at end of file diff --git a/API/Parser/ParserInfo.cs b/API/Parser/ParserInfo.cs index 4b7d5985e..c5499d797 100644 --- a/API/Parser/ParserInfo.cs +++ b/API/Parser/ParserInfo.cs @@ -7,16 +7,36 @@ namespace API.Parser /// public class ParserInfo { - // This can be multiple + /// + /// Represents the parsed chapters from a file. By default, will be 0 which means nothing could be parsed. + /// The chapters can only be a single float or a range of float ie) 1-2. Mainly floats should be multiples of 0.5 representing specials + /// public string Chapters { get; set; } = ""; + /// + /// Represents the parsed series from the file or folder + /// public string Series { get; set; } = ""; - // This can be multiple + /// + /// Represents the parsed volumes from a file. By default, will be 0 which means that nothing could be parsed. + /// If Volumes is 0 and Chapters is 0, the file is a special. If Chapters is non-zero, then no volume could be parsed. + /// Beastars Vol 3-4 will map to "3-4" + /// The volumes can only be a single int or a range of ints ie) 1-2. Float based volumes are not supported. + /// public string Volumes { get; set; } = ""; + /// + /// Filename of the underlying file + /// Beastars v01 (digital).cbz + /// public string Filename { get; init; } = ""; + /// + /// Full filepath of the underlying file + /// C:/Manga/Beastars v01 (digital).cbz + /// public string FullFilePath { get; set; } = ""; /// - /// that represents the type of the file (so caching service knows how to cache for reading) + /// that represents the type of the file + /// Mainly used to show in the UI and so caching service knows how to cache for reading. /// public MangaFormat Format { get; set; } = MangaFormat.Unknown; @@ -26,8 +46,38 @@ namespace API.Parser public string Edition { get; set; } = ""; /// - /// If the file contains no volume/chapter information and contains Special Keywords + /// If the file contains no volume/chapter information or contains Special Keywords /// public bool IsSpecial { get; set; } = false; + + /// + /// Used for specials or books, stores what the UI should show. + /// Manga does not use this field + /// + public string Title { get; set; } = string.Empty; + + /// + /// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0 + /// + /// + public bool IsSpecialInfo() + { + return (IsSpecial || (Volumes == "0" && Chapters == "0")); + } + + /// + /// Merges non empty/null properties from info2 into this entity. + /// + /// + public void Merge(ParserInfo info2) + { + if (info2 == null) return; + Chapters = string.IsNullOrEmpty(Chapters) || Chapters == "0" ? info2.Chapters: Chapters; + Volumes = string.IsNullOrEmpty(Volumes) || Volumes == "0" ? info2.Volumes : Volumes; + Edition = string.IsNullOrEmpty(Edition) ? info2.Edition : Edition; + Title = string.IsNullOrEmpty(Title) ? info2.Title : Title; + Series = string.IsNullOrEmpty(Series) ? info2.Series : Series; + IsSpecial = IsSpecial || info2.IsSpecial; + } } } \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index ca814beb9..f65bba4ff 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -2,7 +2,6 @@ using System; using System.Threading.Tasks; using API.Data; using API.Entities; -using API.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -40,13 +39,6 @@ namespace API var logger = services.GetRequiredService < ILogger>(); logger.LogError(ex, "An error occurred during migration"); } - - // Load all tasks from DI and initialize them (TODO: This is not working - WarmupServicesStartupTask is Null) - var startupTasks = host.Services.GetServices(); - foreach (var startupTask in startupTasks) - { - await startupTask.ExecuteAsync(); - } await host.RunAsync(); } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 2e9f717f1..fc6b8d444 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -21,11 +21,12 @@ namespace API.Services /// /// Responsible for manipulating Archive files. Used by and /// + // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global public class ArchiveService : IArchiveService { private readonly ILogger _logger; private const int ThumbnailWidth = 320; // 153w x 230h - private static readonly RecyclableMemoryStreamManager _streamManager = new(); + private static readonly RecyclableMemoryStreamManager StreamManager = new(); private readonly NaturalSortComparer _comparer; public ArchiveService(ILogger logger) @@ -41,7 +42,7 @@ namespace API.Services /// public virtual ArchiveLibrary CanOpen(string archivePath) { - if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath)) return ArchiveLibrary.NotSupported; + if (!(File.Exists(archivePath) && Parser.Parser.IsArchive(archivePath) || Parser.Parser.IsEpub(archivePath))) return ArchiveLibrary.NotSupported; try { @@ -172,7 +173,7 @@ namespace API.Services var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames); var entry = archive.Entries.Single(e => e.Key == entryName); - using var ms = _streamManager.GetStream(); + using var ms = StreamManager.GetStream(); entry.WriteTo(ms); ms.Position = 0; @@ -197,7 +198,7 @@ namespace API.Services private static byte[] ConvertEntryToByteArray(ZipArchiveEntry entry) { using var stream = entry.Open(); - using var ms = _streamManager.GetStream(); + using var ms = StreamManager.GetStream(); stream.CopyTo(ms); return ms.ToArray(); } @@ -248,7 +249,7 @@ namespace API.Services return false; } - if (Parser.Parser.IsArchive(archivePath)) return true; + if (Parser.Parser.IsArchive(archivePath) || Parser.Parser.IsEpub(archivePath)) return true; _logger.LogError("Archive {ArchivePath} is not a valid archive", archivePath); return false; @@ -261,7 +262,7 @@ namespace API.Services { if (Path.GetFileNameWithoutExtension(entry.Key).ToLower().EndsWith("comicinfo") && !Parser.Parser.HasBlacklistedFolderInPath(entry.Key) && Parser.Parser.IsXml(entry.Key)) { - using var ms = _streamManager.GetStream(); + using var ms = StreamManager.GetStream(); entry.WriteTo(ms); ms.Position = 0; @@ -398,10 +399,10 @@ namespace API.Services break; } case ArchiveLibrary.NotSupported: - _logger.LogError("[GetNumberOfPagesFromArchive] This archive cannot be read: {ArchivePath}. Defaulting to 0 pages", archivePath); + _logger.LogError("[ExtractArchive] This archive cannot be read: {ArchivePath}. Defaulting to 0 pages", archivePath); return; default: - _logger.LogError("[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); + _logger.LogError("[ExtractArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); return; } diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs new file mode 100644 index 000000000..046dbcb40 --- /dev/null +++ b/API/Services/BookService.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using API.Entities.Enums; +using API.Entities.Interfaces; +using API.Interfaces; +using API.Parser; +using ExCSS; +using HtmlAgilityPack; +using Microsoft.Extensions.Logging; +using NetVips; +using VersOne.Epub; +using VersOne.Epub.Schema; + +namespace API.Services +{ + public class BookService : IBookService + { + private readonly ILogger _logger; + + private const int ThumbnailWidth = 320; // 153w x 230h + private readonly StylesheetParser _cssParser = new (); + + public BookService(ILogger logger) + { + _logger = logger; + } + + private static bool HasClickableHrefPart(HtmlNode anchor) + { + return anchor.GetAttributeValue("href", string.Empty).Contains("#") + && anchor.GetAttributeValue("tabindex", string.Empty) != "-1" + && anchor.GetAttributeValue("role", string.Empty) != "presentation"; + } + + public static string GetContentType(EpubContentType type) + { + string contentType; + switch (type) + { + case EpubContentType.IMAGE_GIF: + contentType = "image/gif"; + break; + case EpubContentType.IMAGE_PNG: + contentType = "image/png"; + break; + case EpubContentType.IMAGE_JPEG: + contentType = "image/jpeg"; + break; + case EpubContentType.FONT_OPENTYPE: + contentType = "font/otf"; + break; + case EpubContentType.FONT_TRUETYPE: + contentType = "font/ttf"; + break; + case EpubContentType.IMAGE_SVG: + contentType = "image/svg+xml"; + break; + default: + contentType = "application/octet-stream"; + break; + } + + return contentType; + } + + public static void UpdateLinks(HtmlNode anchor, Dictionary mappings, int currentPage) + { + if (anchor.Name != "a") return; + var hrefParts = BookService.CleanContentKeys(anchor.GetAttributeValue("href", string.Empty)) + .Split("#"); + var mappingKey = hrefParts[0]; + if (!mappings.ContainsKey(mappingKey)) + { + if (HasClickableHrefPart(anchor)) + { + var part = hrefParts.Length > 1 + ? hrefParts[1] + : anchor.GetAttributeValue("href", string.Empty); + anchor.Attributes.Add("kavita-page", $"{currentPage}"); + anchor.Attributes.Add("kavita-part", part); + anchor.Attributes.Remove("href"); + anchor.Attributes.Add("href", "javascript:void(0)"); + } + else + { + anchor.Attributes.Add("target", "_blank"); + } + + return; + } + + var mappedPage = mappings[mappingKey]; + anchor.Attributes.Add("kavita-page", $"{mappedPage}"); + if (hrefParts.Length > 1) + { + anchor.Attributes.Add("kavita-part", + hrefParts[1]); + } + + anchor.Attributes.Remove("href"); + anchor.Attributes.Add("href", "javascript:void(0)"); + } + + public async Task ScopeStyles(string stylesheetHtml, string apiBase) + { + var styleContent = RemoveWhiteSpaceFromStylesheets(stylesheetHtml); + styleContent = + Parser.Parser.FontSrcUrlRegex.Replace(styleContent, "$1" + apiBase + "$2" + "$3"); + + styleContent = styleContent.Replace("body", ".reading-section"); + + var stylesheet = await _cssParser.ParseAsync(styleContent); + foreach (var styleRule in stylesheet.StyleRules) + { + if (styleRule.Selector.Text == ".reading-section") continue; + if (styleRule.Selector.Text.Contains(",")) + { + styleRule.Text = styleRule.Text.Replace(styleRule.SelectorText, + string.Join(", ", + styleRule.Selector.Text.Split(",").Select(s => ".reading-section " + s))); + continue; + } + styleRule.Text = ".reading-section " + styleRule.Text; + } + return RemoveWhiteSpaceFromStylesheets(stylesheet.ToCss()); + } + + public string GetSummaryInfo(string filePath) + { + if (!IsValidFile(filePath)) return string.Empty; + + var epubBook = EpubReader.OpenBook(filePath); + return epubBook.Schema.Package.Metadata.Description; + } + + private bool IsValidFile(string filePath) + { + if (!File.Exists(filePath)) + { + _logger.LogError("Book {EpubFile} could not be found", filePath); + return false; + } + + if (Parser.Parser.IsBook(filePath)) return true; + + _logger.LogError("Book {EpubFile} is not a valid EPUB", filePath); + return false; + } + + public int GetNumberOfPages(string filePath) + { + if (!IsValidFile(filePath) || !Parser.Parser.IsEpub(filePath)) return 0; + + try + { + var epubBook = EpubReader.OpenBook(filePath); + return epubBook.Content.Html.Count; + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception getting number of pages, defaulting to 0"); + } + + return 0; + } + + public static string CleanContentKeys(string key) + { + return key.Replace("../", string.Empty); + } + + public async Task> CreateKeyToPageMappingAsync(EpubBookRef book) + { + var dict = new Dictionary(); + var pageCount = 0; + foreach (var contentFileRef in await book.GetReadingOrderAsync()) + { + if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) continue; + dict.Add(contentFileRef.FileName, pageCount); + pageCount += 1; + } + + return dict; + } + + public static ParserInfo ParseInfo(string filePath) + { + var epubBook = EpubReader.OpenBook(filePath); + + return new ParserInfo() + { + Chapters = "0", + Edition = "", + Format = MangaFormat.Book, + Filename = Path.GetFileName(filePath), + Title = epubBook.Title, + FullFilePath = filePath, + IsSpecial = false, + Series = epubBook.Title, + Volumes = "0" + }; + } + + public byte[] GetCoverImage(string fileFilePath, bool createThumbnail = true) + { + if (!IsValidFile(fileFilePath)) return Array.Empty(); + + var epubBook = EpubReader.OpenBook(fileFilePath); + + + try + { + // Try to get the cover image from OPF file, if not set, try to parse it from all the files, then result to the first one. + var coverImageContent = epubBook.Content.Cover + ?? epubBook.Content.Images.Values.FirstOrDefault(file => Parser.Parser.IsCoverImage(file.FileName)) + ?? epubBook.Content.Images.Values.First(); + + if (coverImageContent == null) return Array.Empty(); + + if (createThumbnail) + { + using var stream = new MemoryStream(coverImageContent.ReadContent()); + + using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth); + return thumbnail.WriteToBuffer(".jpg"); + } + + return coverImageContent.ReadContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); + } + + return Array.Empty(); + } + + private static string RemoveWhiteSpaceFromStylesheets(string body) + { + body = Regex.Replace(body, @"[a-zA-Z]+#", "#"); + body = Regex.Replace(body, @"[\n\r]+\s*", string.Empty); + body = Regex.Replace(body, @"\s+", " "); + body = Regex.Replace(body, @"\s?([:,;{}])\s?", "$1"); + body = body.Replace(";}", "}"); + body = Regex.Replace(body, @"([\s:]0)(px|pt|%|em)", "$1"); + + // Remove comments from CSS + body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty); + + return body; + } + } +} \ No newline at end of file diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 2a2b8e482..4dcad4dc5 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using API.Comparators; using API.Entities; +using API.Entities.Enums; using API.Extensions; using API.Interfaces; using API.Interfaces.Services; @@ -20,7 +21,8 @@ namespace API.Services private readonly NumericComparer _numericComparer; public static readonly string CacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "cache/")); - public CacheService(ILogger logger, IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService) + public CacheService(ILogger logger, IUnitOfWork unitOfWork, IArchiveService archiveService, + IDirectoryService directoryService) { _logger = logger; _unitOfWork = unitOfWork; @@ -31,7 +33,6 @@ namespace API.Services public void EnsureCacheDirectory() { - _logger.LogDebug("Checking if valid Cache directory: {CacheDirectory}", CacheDirectory); if (!DirectoryService.ExistOrCreate(CacheDirectory)) { _logger.LogError("Cache directory {CacheDirectory} is not accessible or does not exist. Creating...", CacheDirectory); @@ -53,7 +54,12 @@ namespace API.Services { extraPath = file.Id + ""; } - _archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath)); + + if (file.Format == MangaFormat.Archive) + { + _archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath)); + } + } if (fileCount > 1) @@ -123,6 +129,11 @@ namespace API.Services var path = GetCachePath(chapter.Id); var files = _directoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions); Array.Sort(files, _numericComparer); + + if (files.Length == 0) + { + return (files.ElementAt(0), mangaFile); + } // Since array is 0 based, we need to keep that in account (only affects last image) if (page == files.Length) diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 1049e3aae..95a48a29e 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -58,8 +58,7 @@ namespace API.Services { rootPath = rootPath.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } - // NOTE: I Could use Path.GetRelativePath and split on separator character instead. - + var path = fullPath.EndsWith(separator) ? fullPath.Substring(0, fullPath.Length - 1) : fullPath; var root = rootPath.EndsWith(separator) ? rootPath.Substring(0, rootPath.Length - 1) : rootPath; var paths = new List(); @@ -215,9 +214,9 @@ namespace API.Services /// Action to apply on file path /// Regex pattern to search against /// - public static int TraverseTreeParallelForEach(string root, Action action, string searchPattern) - { - //Count of files traversed and timer for diagnostic output + public static int TraverseTreeParallelForEach(string root, Action action, string searchPattern, ILogger logger) + { + //Count of files traversed and timer for diagnostic output var fileCount = 0; // Determine whether to parallelize file processing on each folder based on processor count. @@ -242,11 +241,13 @@ namespace API.Services // Thrown if we do not have discovery permission on the directory. catch (UnauthorizedAccessException e) { Console.WriteLine(e.Message); + logger.LogError(e, "Unauthorized access on {Directory}", currentDir); continue; } // Thrown if another process has deleted the directory after we retrieved its name. catch (DirectoryNotFoundException e) { Console.WriteLine(e.Message); + logger.LogError(e, "Directory not found on {Directory}", currentDir); continue; } @@ -268,24 +269,27 @@ namespace API.Services } // Execute in parallel if there are enough files in the directory. - // Otherwise, execute sequentially.Files are opened and processed + // Otherwise, execute sequentially. Files are opened and processed // synchronously but this could be modified to perform async I/O. try { - if (files.Length < procCount) { - foreach (var file in files) { - action(file); - fileCount++; - } - } - else { - Parallel.ForEach(files, () => 0, (file, _, localCount) => - { action(file); - return ++localCount; - }, - (c) => { - // ReSharper disable once AccessToModifiedClosure - Interlocked.Add(ref fileCount, c); - }); + // if (files.Length < procCount) { + // foreach (var file in files) { + // action(file); + // fileCount++; + // } + // } + // else { + // Parallel.ForEach(files, () => 0, (file, _, localCount) => + // { action(file); + // return ++localCount; + // }, + // (c) => { + // Interlocked.Add(ref fileCount, c); + // }); + // } + foreach (var file in files) { + action(file); + fileCount++; } } catch (AggregateException ae) { diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index f86cb3595..5e1f125bb 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -5,6 +5,8 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using API.Entities; +using API.Entities.Enums; +using API.Entities.Interfaces; using API.Extensions; using API.Interfaces; using API.Interfaces.Services; @@ -17,12 +19,14 @@ namespace API.Services private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IArchiveService _archiveService; + private readonly IBookService _bookService; - public MetadataService(IUnitOfWork unitOfWork, ILogger logger, IArchiveService archiveService) + public MetadataService(IUnitOfWork unitOfWork, ILogger logger, IArchiveService archiveService, IBookService bookService) { _unitOfWork = unitOfWork; _logger = logger; _archiveService = archiveService; + _bookService = bookService; } private static bool ShouldFindCoverImage(byte[] coverImage, bool forceUpdate = false) @@ -30,13 +34,25 @@ namespace API.Services return forceUpdate || coverImage == null || !coverImage.Any(); } + private byte[] GetCoverImage(MangaFile file, bool createThumbnail = true) + { + if (file.Format == MangaFormat.Book) + { + return _bookService.GetCoverImage(file.FilePath, createThumbnail); + } + else + { + return _archiveService.GetCoverImage(file.FilePath, createThumbnail); + } + } + public void UpdateMetadata(Chapter chapter, bool forceUpdate) { var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault(); if (ShouldFindCoverImage(chapter.CoverImage, forceUpdate) && firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified)) { chapter.Files ??= new List(); - chapter.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true); + chapter.CoverImage = GetCoverImage(firstFile); } } @@ -55,7 +71,7 @@ namespace API.Services var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault(); if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified)) { - volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true); + volume.CoverImage = GetCoverImage(firstFile); } } else @@ -72,7 +88,7 @@ namespace API.Services if (ShouldFindCoverImage(series.CoverImage, forceUpdate)) { series.Volumes ??= new List(); - var firstCover = series.Volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0); + var firstCover = series.Volumes.GetCoverImage(series.Library.Type); byte[] coverImage = null; if (firstCover == null && series.Volumes.Any()) { @@ -92,24 +108,33 @@ namespace API.Services series.CoverImage = firstCover?.CoverImage ?? coverImage; } + UpdateSeriesSummary(series, forceUpdate); + } + + private void UpdateSeriesSummary(Series series, bool forceUpdate) + { if (!string.IsNullOrEmpty(series.Summary) && !forceUpdate) return; - var firstVolume = series.Volumes.FirstOrDefault(v => v.Chapters.Any() && v.Number == 1); - var firstChapter = firstVolume?.Chapters.FirstOrDefault(c => c.Files.Any()); - + var isBook = series.Library.Type == LibraryType.Book; + var firstVolume = series.Volumes.FirstWithChapters(isBook); + var firstChapter = firstVolume?.Chapters.GetFirstChapterWithFiles(); + + // NOTE: This suffers from code changes not taking effect due to stale data var firstFile = firstChapter?.Files.FirstOrDefault(); - if (firstFile != null && !new FileInfo(firstFile.FilePath).DoesLastWriteMatch(firstFile.LastModified)) + if (firstFile != null && + (forceUpdate || !firstFile.HasFileBeenModified())) { - series.Summary = _archiveService.GetSummaryInfo(firstFile.FilePath); + series.Summary = isBook ? _bookService.GetSummaryInfo(firstFile.FilePath) : _archiveService.GetSummaryInfo(firstFile.FilePath); + firstFile.LastModified = DateTime.Now; } } - - + + public void RefreshMetadata(int libraryId, bool forceUpdate = false) { var sw = Stopwatch.StartNew(); - var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).Result; + var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult(); // TODO: See if we can break this up into multiple threads that process 20 series at a time then save so we can reduce amount of memory used _logger.LogInformation("Beginning metadata refresh of {LibraryName}", library.Name); diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 23936db8a..8857865c0 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -20,7 +20,7 @@ namespace API.Services private readonly ICleanupService _cleanupService; public static BackgroundJobServer Client => new BackgroundJobServer(); - + public TaskScheduler(ICacheService cacheService, ILogger logger, IScannerService scannerService, IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService) @@ -32,20 +32,19 @@ namespace API.Services _metadataService = metadataService; _backupService = backupService; _cleanupService = cleanupService; - - ScheduleTasks(); } public void ScheduleTasks() { _logger.LogInformation("Scheduling reoccurring tasks"); - string setting = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).GetAwaiter().GetResult().Value; + var setting = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).GetAwaiter().GetResult().Value; if (setting != null) { - _logger.LogDebug("Scheduling Scan Library Task for {Setting}", setting); + var scanLibrarySetting = setting; + _logger.LogDebug("Scheduling Scan Library Task for {Setting}", scanLibrarySetting); RecurringJob.AddOrUpdate("scan-libraries", () => _scannerService.ScanLibraries(), - () => CronConverter.ConvertToCronNotation(setting)); + () => CronConverter.ConvertToCronNotation(scanLibrarySetting)); } else { @@ -69,7 +68,7 @@ namespace API.Services public void ScanLibrary(int libraryId, bool forceUpdate = false) { _logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId); - BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate)); + BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate)); // When we do a scan, force cache to re-unpack in case page numbers change BackgroundJob.Enqueue(() => _cleanupService.Cleanup()); } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index bc812bf00..b6ca2c2be 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -6,8 +6,10 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using API.Comparators; +using API.Data; using API.Entities; using API.Entities.Enums; +using API.Entities.Interfaces; using API.Extensions; using API.Interfaces; using API.Interfaces.Services; @@ -23,17 +25,19 @@ namespace API.Services.Tasks private readonly ILogger _logger; private readonly IArchiveService _archiveService; private readonly IMetadataService _metadataService; + private readonly IBookService _bookService; private ConcurrentDictionary> _scannedSeries; private readonly NaturalSortComparer _naturalSort; public ScannerService(IUnitOfWork unitOfWork, ILogger logger, IArchiveService archiveService, - IMetadataService metadataService) + IMetadataService metadataService, IBookService bookService) { _unitOfWork = unitOfWork; _logger = logger; _archiveService = archiveService; _metadataService = metadataService; - _naturalSort = new NaturalSortComparer(true); + _bookService = bookService; + _naturalSort = new NaturalSortComparer(); } @@ -43,13 +47,14 @@ namespace API.Services.Tasks var libraries = Task.Run(() => _unitOfWork.LibraryRepository.GetLibrariesAsync()).Result.ToList(); foreach (var lib in libraries) { + // BUG?: I think we need to keep _scannedSeries within the ScanLibrary instance since this is multithreaded. ScanLibrary(lib.Id, false); } } private bool ShouldSkipFolderScan(FolderPath folder, ref int skippedFolders) { - // NOTE: This solution isn't the best, but it has potential. We need to handle a few other cases so it works great. + // NOTE: The only way to skip folders is if Directory hasn't been modified, we aren't doing a forcedUpdate and version hasn't changed between scans. return false; // if (!_forceUpdate && Directory.GetLastWriteTime(folder.Path) < folder.LastScanned) @@ -66,6 +71,7 @@ namespace API.Services.Tasks public void ScanLibrary(int libraryId, bool forceUpdate) { var sw = Stopwatch.StartNew(); + _scannedSeries = new ConcurrentDictionary>(); Library library; try { @@ -79,260 +85,281 @@ namespace API.Services.Tasks } - _logger.LogInformation("Beginning scan on {LibraryName}. Forcing metadata update: {ForceUpdate}", library.Name, forceUpdate); - - _scannedSeries = new ConcurrentDictionary>(); - - var totalFiles = 0; - var skippedFolders = 0; - foreach (var folderPath in library.Folders) - { - if (ShouldSkipFolderScan(folderPath, ref skippedFolders)) continue; - - try { - totalFiles += DirectoryService.TraverseTreeParallelForEach(folderPath.Path, (f) => - { - try - { - ProcessFile(f, folderPath.Path, library.Type); - } - catch (FileNotFoundException exception) - { - _logger.LogError(exception, "The file {Filename} could not be found", f); - } - }, Parser.Parser.ArchiveFileExtensions); - } - catch (ArgumentException ex) { - _logger.LogError(ex, "The directory '{FolderPath}' does not exist", folderPath.Path); - } - - folderPath.LastScanned = DateTime.Now; - } - - var scanElapsedTime = sw.ElapsedMilliseconds; - _logger.LogInformation("Folders Scanned {TotalFiles} files in {ElapsedScanTime} milliseconds", totalFiles, scanElapsedTime); - sw.Restart(); - if (skippedFolders == library.Folders.Count) - { - _logger.LogInformation("All Folders were skipped due to no modifications to the directories"); - _unitOfWork.LibraryRepository.Update(library); - _scannedSeries = null; - _logger.LogInformation("Processed {TotalFiles} files in {ElapsedScanTime} milliseconds for {LibraryName}", totalFiles, sw.ElapsedMilliseconds, library.Name); - return; - } - - // Remove any series where there were no parsed infos - var filtered = _scannedSeries.Where(kvp => kvp.Value.Count != 0); - var series = filtered.ToDictionary(v => v.Key, v => v.Value); - + var series = ScanLibrariesForSeries(forceUpdate, library, sw, out var totalFiles, out var scanElapsedTime); UpdateLibrary(library, series); + _unitOfWork.LibraryRepository.Update(library); - if (Task.Run(() => _unitOfWork.Complete()).Result) { - _logger.LogInformation("Scan completed on {LibraryName}. Parsed {ParsedSeriesCount} series in {ElapsedScanTime} ms", library.Name, series.Keys.Count, sw.ElapsedMilliseconds); + _logger.LogInformation("Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", totalFiles, series.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, library.Name); } else { - _logger.LogError("There was a critical error that resulted in a failed scan. Please check logs and rescan"); + _logger.LogCritical("There was a critical error that resulted in a failed scan. Please check logs and rescan"); } - _scannedSeries = null; - - _logger.LogInformation("Processed {TotalFiles} files in {ElapsedScanTime} milliseconds for {LibraryName}", totalFiles, sw.ElapsedMilliseconds + scanElapsedTime, library.Name); - - // Cleanup any user progress that doesn't exist - var cleanedUp = Task.Run(() => _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters()).Result; - _logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp); + + CleanupUserProgress(); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate)); } - + /// + /// Remove any user progress rows that no longer exist since scan library ran and deleted series/volumes/chapters + /// + private void CleanupUserProgress() + { + var cleanedUp = Task.Run(() => _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters()).Result; + _logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp); + } + + private Dictionary> ScanLibrariesForSeries(bool forceUpdate, Library library, Stopwatch sw, out int totalFiles, + out long scanElapsedTime) + { + _logger.LogInformation("Beginning scan on {LibraryName}. Forcing metadata update: {ForceUpdate}", library.Name, + forceUpdate); + totalFiles = 0; + var skippedFolders = 0; + foreach (var folderPath in library.Folders) + { + if (ShouldSkipFolderScan(folderPath, ref skippedFolders)) continue; + + // NOTE: we can refactor this to allow all filetypes and handle everything in the ProcessFile to allow mixed library types. + var searchPattern = Parser.Parser.ArchiveFileExtensions; + if (library.Type == LibraryType.Book) + { + searchPattern = Parser.Parser.BookFileExtensions; + } + + try + { + totalFiles += DirectoryService.TraverseTreeParallelForEach(folderPath.Path, (f) => + { + try + { + ProcessFile(f, folderPath.Path, library.Type); + } + catch (FileNotFoundException exception) + { + _logger.LogError(exception, "The file {Filename} could not be found", f); + } + }, searchPattern, _logger); + } + catch (ArgumentException ex) + { + _logger.LogError(ex, "The directory '{FolderPath}' does not exist", folderPath.Path); + } + + folderPath.LastScanned = DateTime.Now; + } + + scanElapsedTime = sw.ElapsedMilliseconds; + _logger.LogInformation("Folders Scanned {TotalFiles} files in {ElapsedScanTime} milliseconds", totalFiles, + scanElapsedTime); + sw.Restart(); + if (skippedFolders == library.Folders.Count) + { + _logger.LogInformation("All Folders were skipped due to no modifications to the directories"); + _unitOfWork.LibraryRepository.Update(library); + _scannedSeries = null; + _logger.LogInformation("Processed {TotalFiles} files in {ElapsedScanTime} milliseconds for {LibraryName}", + totalFiles, sw.ElapsedMilliseconds, library.Name); + return new Dictionary>(); + } + + return SeriesWithInfos(_scannedSeries); + } + + /// + /// Returns any series where there were parsed infos + /// + /// + /// + private static Dictionary> SeriesWithInfos(IDictionary> scannedSeries) + { + var filtered = scannedSeries.Where(kvp => kvp.Value.Count > 0); + var series = filtered.ToDictionary(v => v.Key, v => v.Value); + return series; + } + + private void UpdateLibrary(Library library, Dictionary> parsedSeries) { if (parsedSeries == null) throw new ArgumentNullException(nameof(parsedSeries)); - + // First, remove any series that are not in parsedSeries list - var missingSeries = FindSeriesNotOnDisk(library.Series, parsedSeries); - var removeCount = RemoveMissingSeries(library.Series, missingSeries); - _logger.LogInformation("Removed {RemoveMissingSeries} series that are no longer on disk", removeCount); + var missingSeries = FindSeriesNotOnDisk(library.Series, parsedSeries).ToList(); + library.Series = RemoveMissingSeries(library.Series, missingSeries, out var removeCount); + if (removeCount > 0) + { + _logger.LogInformation("Removed {RemoveMissingSeries} series that are no longer on disk:", removeCount); + foreach (var s in missingSeries) + { + _logger.LogDebug("Removed {SeriesName}", s.Name); + } + } + // Add new series that have parsedInfos foreach (var (key, infos) in parsedSeries) { - var existingSeries = library.Series.SingleOrDefault(s => s.NormalizedName == Parser.Parser.Normalize(key)); + // Key is normalized already + var existingSeries = library.Series.SingleOrDefault(s => s.NormalizedName == key || Parser.Parser.Normalize(s.OriginalName) == key); if (existingSeries == null) { - var name = infos.Count > 0 ? infos[0].Series : key; - existingSeries = new Series() - { - Name = name, - OriginalName = name, - LocalizedName = name, - NormalizedName = Parser.Parser.Normalize(key), - SortName = key, - Summary = "", - Volumes = new List() - }; + existingSeries = DbFactory.Series(infos[0].Series); library.Series.Add(existingSeries); - } - existingSeries.NormalizedName = Parser.Parser.Normalize(key); - existingSeries.LocalizedName ??= key; + } + + existingSeries.NormalizedName = Parser.Parser.Normalize(existingSeries.Name); + existingSeries.OriginalName ??= infos[0].Series; } // Now, we only have to deal with series that exist on disk. Let's recalculate the volumes for each series var librarySeries = library.Series.ToList(); Parallel.ForEach(librarySeries, (series) => { - _logger.LogInformation("Processing series {SeriesName}", series.Name); - UpdateVolumes(series, parsedSeries[Parser.Parser.Normalize(series.OriginalName)].ToArray()); - series.Pages = series.Volumes.Sum(v => v.Pages); + try + { + _logger.LogInformation("Processing series {SeriesName}", series.OriginalName); + UpdateVolumes(series, parsedSeries[Parser.Parser.Normalize(series.OriginalName)].ToArray()); + series.Pages = series.Volumes.Sum(v => v.Pages); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception updating volumes for {SeriesName}", series.Name); + } }); } public IEnumerable FindSeriesNotOnDisk(ICollection existingSeries, Dictionary> parsedSeries) { var foundSeries = parsedSeries.Select(s => s.Key).ToList(); - var missingSeries = existingSeries.Where(es => !es.NameInList(foundSeries) - || !es.NameInList(parsedSeries.Keys)); - return missingSeries; + return existingSeries.Where(es => !es.NameInList(foundSeries)); } - public int RemoveMissingSeries(ICollection existingSeries, IEnumerable missingSeries) + /// + /// Removes all instances of missingSeries' Series from existingSeries Collection. Existing series is updated by + /// reference and the removed element count is returned. + /// + /// Existing Series in DB + /// Series not found on disk or can't be parsed + /// + /// the updated existingSeries + public static ICollection RemoveMissingSeries(ICollection existingSeries, IEnumerable missingSeries, out int removeCount) { - - var removeCount = existingSeries.Count; + var existingCount = existingSeries.Count; var missingList = missingSeries.ToList(); - existingSeries = existingSeries.Except(missingList).ToList(); - // if (existingSeries == null || existingSeries.Count == 0) return 0; - // foreach (var existing in missingSeries) - // { - // existingSeries.Remove(existing); - // removeCount += 1; - // } - removeCount -= existingSeries.Count; + + existingSeries = existingSeries.Where( + s => !missingList.Exists( + m => m.NormalizedName.Equals(s.NormalizedName))).ToList(); - return removeCount; + removeCount = existingCount - existingSeries.Count; + + return existingSeries; } private void UpdateVolumes(Series series, ParserInfo[] parsedInfos) { var startingVolumeCount = series.Volumes.Count; // Add new volumes and update chapters per volume - var distinctVolumes = parsedInfos.Select(p => p.Volumes).Distinct().ToList(); - _logger.LogDebug("Updating {DistinctVolumes} volumes", distinctVolumes.Count); + var distinctVolumes = parsedInfos.DistinctVolumes(); + _logger.LogDebug("Updating {DistinctVolumes} volumes on {SeriesName}", distinctVolumes.Count, series.Name); foreach (var volumeNumber in distinctVolumes) { - var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray(); - var volume = series.Volumes.SingleOrDefault(s => s.Name == volumeNumber); if (volume == null) { - volume = new Volume() - { - Name = volumeNumber, - Number = (int) Parser.Parser.MinimumNumberFromRange(volumeNumber), - IsSpecial = false, - Chapters = new List() - }; + volume = DbFactory.Volume(volumeNumber); series.Volumes.Add(volume); } - // NOTE: I don't think we need this as chapters now handle specials - //volume.IsSpecial = volume.Number == 0 && infos.All(p => p.Chapters == "0" || p.IsSpecial); + // NOTE: Instead of creating and adding? Why Not Merge a new volume into an existing, so no matter what, new properties,etc get propagated? + _logger.LogDebug("Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name); - + var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray(); UpdateChapters(volume, infos); volume.Pages = volume.Chapters.Sum(c => c.Pages); } - - // BUG: This is causing volumes to be removed when they shouldn't - // Remove existing volumes that aren't in parsedInfos and volumes that have no chapters - var existingVolumeLength = series.Volumes.Count; - // var existingVols = series.Volumes; - // foreach (var v in existingVols) - // { - // // NOTE: I think checking if Chapter count is 0 is enough, we don't need parsedInfos - // if (parsedInfos.All(p => p.Volumes != v.Name)) // || v.Chapters.Count == 0 (this wont work yet because we don't take care of chapters correctly vs parsedInfos) - // { - // _logger.LogDebug("Removed {Series} - {Volume} as there were no chapters", series.Name, v.Name); - // series.Volumes.Remove(v); - // } - // } - series.Volumes = series.Volumes.Where(v => parsedInfos.Any(p => p.Volumes == v.Name)).ToList(); - if (existingVolumeLength != series.Volumes.Count) + + // Remove existing volumes that aren't in parsedInfos + var nonDeletedVolumes = series.Volumes.Where(v => parsedInfos.Select(p => p.Volumes).Contains(v.Name)).ToList(); + if (series.Volumes.Count != nonDeletedVolumes.Count) { - _logger.LogDebug("Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name", (existingVolumeLength - series.Volumes.Count), series.Name); + _logger.LogDebug("Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name", + (series.Volumes.Count - nonDeletedVolumes.Count), series.Name); + var deletedVolumes = series.Volumes.Except(nonDeletedVolumes); + foreach (var volume in deletedVolumes) + { + var file = volume.Chapters.FirstOrDefault()?.Files.FirstOrDefault()?.FilePath ?? "no files"; + if (!new FileInfo(file).Exists) + { + _logger.LogError("Volume cleanup code was trying to remove a volume with a file still existing on disk. File: {File}", file); + } + _logger.LogDebug("Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file); + } + + series.Volumes = nonDeletedVolumes; } _logger.LogDebug("Updated {SeriesName} volumes from {StartingVolumeCount} to {VolumeCount}", series.Name, startingVolumeCount, series.Volumes.Count); } - + + /// + /// + /// + /// + /// private void UpdateChapters(Volume volume, ParserInfo[] parsedInfos) { - var startingChapters = volume.Chapters.Count; - // Add new chapters foreach (var info in parsedInfos) { - var specialTreatment = (info.IsSpecial || (info.Volumes == "0" && info.Chapters == "0")); // Specials go into their own chapters with Range being their filename and IsSpecial = True. Non-Specials with Vol and Chap as 0 // also are treated like specials for UI grouping. - // NOTE: If there are duplicate files that parse out to be the same but a different series name (but parses to same normalized name ie History's strongest - // vs Historys strongest), this code will break and the duplicate will be skipped. - Chapter chapter = null; + Chapter chapter; try { - // TODO: Extract to FindExistingChapter() - chapter = specialTreatment - ? volume.Chapters.SingleOrDefault(c => c.Range == info.Filename - || (c.Files.Select(f => f.FilePath) - .Contains(info.FullFilePath))) - : volume.Chapters.SingleOrDefault(c => c.Range == info.Chapters); + chapter = volume.Chapters.GetChapterByRange(info); } catch (Exception ex) { _logger.LogError(ex, "{FileName} mapped as '{Series} - Vol {Volume} Ch {Chapter}' is a duplicate, skipping", info.FullFilePath, info.Series, info.Volumes, info.Chapters); - return; + continue; } - - + if (chapter == null) { - _logger.LogDebug("Adding new chapter, {Series} - Vol {Volume} Ch {Chapter} - Needs Special Treatment? {NeedsSpecialTreatment}", info.Series, info.Volumes, info.Chapters, specialTreatment); - chapter = new Chapter() - { - Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty, - Range = specialTreatment ? info.Filename : info.Chapters, - Files = new List(), - IsSpecial = specialTreatment - }; - volume.Chapters.Add(chapter); + _logger.LogDebug( + "Adding new chapter, {Series} - Vol {Volume} Ch {Chapter}", info.Series, info.Volumes, info.Chapters); + volume.Chapters.Add(DbFactory.Chapter(info)); } - - chapter.Files ??= new List(); - chapter.IsSpecial = specialTreatment; + else + { + chapter.UpdateFrom(info); + } + } // Add files foreach (var info in parsedInfos) { - var specialTreatment = (info.IsSpecial || (info.Volumes == "0" && info.Chapters == "0")); + var specialTreatment = info.IsSpecialInfo(); Chapter chapter = null; try { - chapter = volume.Chapters.SingleOrDefault(c => c.Range == info.Chapters || (specialTreatment && c.Range == info.Filename)); + chapter = volume.Chapters.GetChapterByRange(info); } catch (Exception ex) { _logger.LogError(ex, "There was an exception parsing chapter. Skipping {SeriesName} Vol {VolumeNumber} Chapter {ChapterNumber} - Special treatment: {NeedsSpecialTreatment}", info.Series, volume.Name, info.Chapters, specialTreatment); + continue; } if (chapter == null) continue; AddOrUpdateFileForChapter(chapter, info); chapter.Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty; chapter.Range = specialTreatment ? info.Filename : info.Chapters; - chapter.Pages = chapter.Files.Sum(f => f.Pages); } @@ -340,11 +367,7 @@ namespace API.Services.Tasks var existingChapters = volume.Chapters.ToList(); foreach (var existingChapter in existingChapters) { - var specialTreatment = (existingChapter.IsSpecial || (existingChapter.Number == "0" && !int.TryParse(existingChapter.Range, out int i))); - var hasInfo = specialTreatment ? parsedInfos.Any(v => v.Filename == existingChapter.Range) - : parsedInfos.Any(v => v.Chapters == existingChapter.Range); - - if (!hasInfo || existingChapter.Files.Count == 0) + if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter)) { _logger.LogDebug("Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series); volume.Chapters.Remove(existingChapter); @@ -355,13 +378,9 @@ namespace API.Services.Tasks existingChapter.Files = existingChapter.Files .Where(f => parsedInfos.Any(p => p.FullFilePath == f.FilePath)) .OrderBy(f => f.FilePath, _naturalSort).ToList(); + existingChapter.Pages = existingChapter.Files.Sum(f => f.Pages); } } - - - - _logger.LogDebug("Updated chapters from {StartingChaptersCount} to {ChapterCount}", - startingChapters, volume.Chapters.Count); } /// @@ -393,7 +412,8 @@ namespace API.Services.Tasks _logger.LogDebug("Checking if we can merge {NormalizedSeries}", normalizedSeries); var existingName = collectedSeries.SingleOrDefault(p => Parser.Parser.Normalize(p.Key) == normalizedSeries) .Key; - if (!string.IsNullOrEmpty(existingName) && info.Series != existingName) + // BUG: We are comparing info.Series against a normalized string. They should never match. (This can cause series to not delete or parse correctly after a rename) + if (!string.IsNullOrEmpty(existingName)) // && info.Series != existingName { _logger.LogDebug("Found duplicate parsed infos, merged {Original} into {Merged}", info.Series, existingName); return existingName; @@ -411,25 +431,61 @@ namespace API.Services.Tasks /// Library type to determine parsing to perform private void ProcessFile(string path, string rootPath, LibraryType type) { - var info = Parser.Parser.Parse(path, rootPath, type); + ParserInfo info; + if (type == LibraryType.Book && Parser.Parser.IsEpub(path)) + { + info = BookService.ParseInfo(path); + } + else + { + info = Parser.Parser.Parse(path, rootPath, type); + } + if (info == null) { _logger.LogWarning("[Scanner] Could not parse series from {Path}", path); return; } + if (type == LibraryType.Book && Parser.Parser.IsEpub(path) && Parser.Parser.ParseVolume(info.Series) != "0") + { + info = Parser.Parser.Parse(path, rootPath, type); + var info2 = BookService.ParseInfo(path); + info.Merge(info2); + } + TrackSeries(info); } private MangaFile CreateMangaFile(ParserInfo info) { - return new MangaFile() + switch (info.Format) { - FilePath = info.FullFilePath, - Format = info.Format, - Pages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath) - }; + case MangaFormat.Archive: + { + return new MangaFile() + { + FilePath = info.FullFilePath, + Format = info.Format, + Pages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath) + }; + } + case MangaFormat.Book: + { + return new MangaFile() + { + FilePath = info.FullFilePath, + Format = info.Format, + Pages = _bookService.GetNumberOfPages(info.FullFilePath) + }; + } + default: + _logger.LogWarning("[Scanner] Ignoring {Filename}. Non-archives are not supported", info.Filename); + break; + } + + return null; } private void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info) @@ -439,22 +495,21 @@ namespace API.Services.Tasks if (existingFile != null) { existingFile.Format = info.Format; - if (!new FileInfo(existingFile.FilePath).DoesLastWriteMatch(existingFile.LastModified)) + if (!existingFile.HasFileBeenModified() && existingFile.Pages > 0) { - existingFile.Pages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath); + existingFile.Pages = existingFile.Format == MangaFormat.Book + ? _bookService.GetNumberOfPages(info.FullFilePath) + : _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath); } } else { - if (info.Format == MangaFormat.Archive) + var file = CreateMangaFile(info); + if (file != null) { - chapter.Files.Add(CreateMangaFile(info)); + chapter.Files.Add(file); existingFile = chapter.Files.Last(); } - else - { - _logger.LogDebug("Ignoring {Filename}. Non-archives are not supported", info.Filename); - } } if (existingFile != null) diff --git a/API/Startup.cs b/API/Startup.cs index b2a06c866..4d26d933e 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -2,7 +2,7 @@ using System; using System.IO.Compression; using System.Linq; using API.Extensions; -using API.Interfaces.Services; +using API.Interfaces; using API.Middleware; using API.Services; using Hangfire; @@ -15,7 +15,6 @@ using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; @@ -24,12 +23,10 @@ namespace API public class Startup { private readonly IConfiguration _config; - private readonly IWebHostEnvironment _env; - public Startup(IConfiguration config, IWebHostEnvironment env) + public Startup(IConfiguration config) { _config = config; - _env = env; } // This method gets called by the runtime. Use this method to add services to the container. @@ -71,16 +68,14 @@ namespace API // Add the processing server as IHostedService services.AddHangfireServer(); - - //services.AddStartupTask(services). - services.AddTransient().TryAddSingleton(services); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime) + public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env, + IHostApplicationLifetime applicationLifetime, ITaskScheduler taskScheduler) { app.UseMiddleware(); - + if (env.IsDevelopment()) { app.UseSwagger(); @@ -136,8 +131,11 @@ namespace API applicationLifetime.ApplicationStopping.Register(OnShutdown); applicationLifetime.ApplicationStarted.Register(() => { - Console.WriteLine("Kavita - v0.3.7"); + Console.WriteLine("Kavita - v0.4.0"); }); + + // Any services that should be bootstrapped go here + taskScheduler.ScheduleTasks(); } private void OnShutdown() diff --git a/INSTALL.txt b/INSTALL.txt index 5d052dccd..a8b83f905 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -1,4 +1,5 @@ How to Install -1. Unzip the archive to a directory that is writable. If on windows, do not place in Program Files -2. Open appsettings.json and modify TokenKey to a random string ideally generated from https://passwordsgenerator.net/ -3. Run Kavita executable \ No newline at end of file +1. Unzip the archive to a directory that is writable. If on windows, do not place in Program Files. +2. (Linux only) Chmod and Chown so Kavita can write to the directory you placed in. +3. Open appsettings.json and modify TokenKey to a random string ideally generated from https://passwordsgenerator.net/ +4. Run Kavita executable \ No newline at end of file diff --git a/build.sh b/build.sh index 51b68e571..d10013968 100644 --- a/build.sh +++ b/build.sh @@ -47,10 +47,10 @@ Build() BuildUI() { ProgressStart 'Building UI' - cd ../kavita-webui/ || exit + cd ../Kavita-webui/ || exit npm install npm run prod - cd ../kavita/ || exit + cd ../Kavita/ || exit ProgressEnd 'Building UI' } @@ -113,6 +113,3 @@ else Package "net5.0" "$RID" cd "$dir" fi - - -