From f17d89ea47f2edac8b7f2326a100305c770547a8 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 4 Oct 2021 16:10:48 -0700 Subject: [PATCH 01/10] ComicInfo Refactor (#636) * Implemented methods to parse out the whole ComicInfo file and a mock ComicInfo from epub files. * Removed unused imports. ScanSeries needs to cleanup deleted chapters and call RefreshMetadata. Ensure after scan we cleanup tags without any series. * Random cleanup * Added some comments about data getting stale and not updating. * Removed old Summary methods in favor of the ComicInfo versions * Added a missing property * Fixed unit test --- API.Benchmark/ParseScannedFilesBenchmarks.cs | 8 +-- API.Benchmark/ParserBenchmarks.cs | 3 +- API.Benchmark/Program.cs | 2 +- API.Tests/Services/ArchiveServiceTests.cs | 25 +++++++- API.Tests/Services/DirectoryServiceTests.cs | 2 +- API.Tests/Services/MetadataServiceTests.cs | 6 -- .../ArchiveService/ComicInfos/ComicInfo.zip | Bin 0 -> 854 bytes API/Controllers/ImageController.cs | 5 +- API/Controllers/OPDSController.cs | 2 - API/Controllers/SettingsController.cs | 2 +- API/Controllers/UploadController.cs | 1 - API/DTOs/OPDS/FeedLink.cs | 2 +- API/DTOs/Reader/IChapterInfoDto.cs | 1 - API/DTOs/Settings/ServerSettingDTO.cs | 2 +- API/Data/Metadata/ComicInfo.cs | 51 ++++++++++++++++ API/Data/Repositories/ChapterRepository.cs | 2 - .../Repositories/CollectionTagRepository.cs | 1 - API/Data/Repositories/SeriesRepository.cs | 1 - API/Data/Repositories/SettingsRepository.cs | 2 +- API/Entities/AppUserProgress.cs | 1 - API/Entities/Volume.cs | 1 - API/Helpers/AutoMapperProfiles.cs | 1 + .../Converters/ServerSettingConverter.cs | 2 +- .../Repositories/ISeriesRepository.cs | 4 +- .../Repositories/ISettingsRepository.cs | 2 +- API/Interfaces/Services/IArchiveService.cs | 3 +- API/Interfaces/Services/IBookService.cs | 3 +- API/Interfaces/Services/ReaderService.cs | 1 - API/Program.cs | 8 --- API/Services/ArchiveService.cs | 56 ++++++++++-------- API/Services/BookService.cs | 35 ++++++++--- API/Services/ComicInfo.cs | 16 ----- API/Services/ImageService.cs | 2 +- API/Services/MetadataService.cs | 26 +++++--- API/Services/Tasks/BackupService.cs | 2 +- API/Services/Tasks/CleanupService.cs | 2 - API/Services/Tasks/ScannerService.cs | 17 +++--- 37 files changed, 181 insertions(+), 119 deletions(-) create mode 100644 API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip create mode 100644 API/Data/Metadata/ComicInfo.cs delete mode 100644 API/Services/ComicInfo.cs diff --git a/API.Benchmark/ParseScannedFilesBenchmarks.cs b/API.Benchmark/ParseScannedFilesBenchmarks.cs index d3fd19a4e..8681c1261 100644 --- a/API.Benchmark/ParseScannedFilesBenchmarks.cs +++ b/API.Benchmark/ParseScannedFilesBenchmarks.cs @@ -1,6 +1,4 @@ -using System; -using System.IO; -using API.Data; +using System.IO; using API.Entities.Enums; using API.Interfaces.Services; using API.Parser; @@ -57,8 +55,8 @@ namespace API.Benchmark Title = "A Town Where You Live", Volumes = "1" }; - var parsedSeries = _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new string[] {libraryPath}, - out var totalFiles, out var scanElapsedTime); + _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath}, + out _, out _); _parseScannedFiles.MergeName(p1); } } diff --git a/API.Benchmark/ParserBenchmarks.cs b/API.Benchmark/ParserBenchmarks.cs index 8eaa70a28..ef12331cc 100644 --- a/API.Benchmark/ParserBenchmarks.cs +++ b/API.Benchmark/ParserBenchmarks.cs @@ -36,7 +36,8 @@ namespace API.Benchmark private static void NormalizeNew(string name) { - NormalizeRegex.Replace(name, string.Empty).ToLower(); + // ReSharper disable once UnusedVariable + var ret = NormalizeRegex.Replace(name, string.Empty).ToLower(); } diff --git a/API.Benchmark/Program.cs b/API.Benchmark/Program.cs index f12146a7f..c3ef1b605 100644 --- a/API.Benchmark/Program.cs +++ b/API.Benchmark/Program.cs @@ -10,7 +10,7 @@ namespace API.Benchmark /// public static class Program { - static void Main(string[] args) + private static void Main(string[] args) { //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 80f09a144..fc3e21dd4 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -2,6 +2,7 @@ using System.IO; using System.IO.Compression; using API.Archive; +using API.Data.Metadata; using API.Interfaces.Services; using API.Services; using Microsoft.Extensions.Logging; @@ -216,8 +217,30 @@ namespace API.Tests.Services var archive = Path.Join(testDirectory, "file in folder.zip"); var summaryInfo = "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?"; - Assert.Equal(summaryInfo, _archiveService.GetSummaryInfo(archive)); + Assert.Equal(summaryInfo, _archiveService.GetComicInfo(archive).Summary); + } + [Fact] + public void CanParseComicInfo() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var archive = Path.Join(testDirectory, "ComicInfo.zip"); + var actual = _archiveService.GetComicInfo(archive); + var expected = new ComicInfo() + { + Publisher = "Yen Press", + Genre = "Manga, Movies & TV", + Summary = + "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?", + PageCount = 194, + LanguageISO = "en", + Notes = "Scraped metadata from Comixology [CMXDB450184]", + Series = "BTOOOM!", + Title = "v01", + Web = "https://www.comixology.com/BTOOOM/digital-comic/450184" + }; + + Assert.NotStrictEqual(expected, actual); } } } diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index db756ebab..90cf1a217 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -90,7 +90,7 @@ namespace API.Tests.Services } [Theory] - [InlineData(new string[] {"C:/Manga/"}, new string[] {"C:/Manga/Love Hina/Vol. 01.cbz"}, "C:/Manga/Love Hina")] + [InlineData(new [] {"C:/Manga/"}, new [] {"C:/Manga/Love Hina/Vol. 01.cbz"}, "C:/Manga/Love Hina")] public void FindHighestDirectoriesFromFilesTest(string[] rootDirectories, string[] folders, string expectedDirectory) { var actual = DirectoryService.FindHighestDirectoriesFromFiles(rootDirectories, folders); diff --git a/API.Tests/Services/MetadataServiceTests.cs b/API.Tests/Services/MetadataServiceTests.cs index b921f74b7..5d61ee249 100644 --- a/API.Tests/Services/MetadataServiceTests.cs +++ b/API.Tests/Services/MetadataServiceTests.cs @@ -1,13 +1,7 @@ using System; using System.IO; using API.Entities; -using API.Interfaces; -using API.Interfaces.Services; using API.Services; -using API.SignalR; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; -using NSubstitute; using Xunit; namespace API.Tests.Services diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip b/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip new file mode 100644 index 0000000000000000000000000000000000000000..3ff536270eb4c99210fb69ca12c81a767b991ffa GIT binary patch literal 854 zcmWIWW@Zs#U|`^2SmRX@bf+$H+BPNzhLtP~47@;5=ltBvWY4^`e7%a?oYoNE{3`|m zdzC-RFRO0wj(+bnaY4(P7fOsm-mCMDwhOd-8qMCZ)pp`rm9zWP=WJQ}=#c8B=a0{x znJu3FIy!rSy1dEm^6sl2i#DE{Z+Frxr}XE|w~|NRA9|U);-2m-iQB&d;6eM0QZ42}2GSNohjesY)cdd(;4;m@Bx zzuoL}_pbW=a;;Axs&}SpE`1(t7SsQ7#WR`Emr?E}?kA&HO%6YO_4Q<*rFV8Zr0f1N zw3lY=ZJcB+epoT4?~ZGW?7am~Pb^?okl=fgm~q0<`8=~l--Qhexps+dkx)HleA`*$ zhS;sE0@6R(K60{%yL~_OsrHFn^*1Y_4aT!*_<<*5t54Z4hZg`@mc3B`MX7Yzy-P6Ad$Xk4EVRL$3efRW2H|s#JMS8}k($<(s z)iTX)osg0AQqSYH;w1k+u8o)M$|{yiebW6^QKBAtW5Mq5z>ca((?lj(FS)j(cFX!} z4`#I8**t&SJhPe&=dSEEQ(sv3RO8V7sdCXP^E8uMoA!RsN&LK^=Ev5X|9%vho&Hn4 z?AaR52aALm*=~mZ3+9Ti;8F~(?+rfVUNrBM?ye}edqw*HrEHvU?`vJ1KXcYzV~J~9 zJcFOFHveS(cdy?)eWnYYFF(JtQJ=o@`1hadwJ$Ecel*%=^(o`8>s{|DeEq<2@-2UW zHzSig1Fj^l0!-u}pa3S(5 + /// A representation of a ComicInfo.xml file + /// + /// See reference of the loose spec here: https://github.com/Kussie/ComicInfoStandard/blob/main/ComicInfo.xsd + public class ComicInfo + { + public string Summary { get; set; } + public string Title { get; set; } + public string Series { get; set; } + public string Number { get; set; } + public string Volume { get; set; } + public string Notes { get; set; } + public string Genre { get; set; } + public int PageCount { get; set; } + // ReSharper disable once InconsistentNaming + public string LanguageISO { get; set; } + public string Web { get; set; } + public int Month { get; set; } + public int Year { get; set; } + /// + /// Rating based on the content. Think PG-13, R for movies + /// + public string AgeRating { get; set; } + /// + /// User's rating of the content + /// + public float UserRating { get; set; } + + public string AlternateSeries { get; set; } + public string StoryArc { get; set; } + public string SeriesGroup { get; set; } + public string AlternativeSeries { get; set; } + public string AlternativeNumber { get; set; } + + + /// + /// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple. + /// + public string Writer { get; set; } // TODO: Validate if we should make this a list of writers + public string Penciller { get; set; } + public string Inker { get; set; } + public string Colorist { get; set; } + public string Letterer { get; set; } + public string CoverArtist { get; set; } + public string Editor { get; set; } + public string Publisher { get; set; } + + } +} diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index f1905eaa8..54c808d9c 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using API.DTOs; using API.DTOs.Reader; diff --git a/API/Data/Repositories/CollectionTagRepository.cs b/API/Data/Repositories/CollectionTagRepository.cs index 777e82788..2766e6ba7 100644 --- a/API/Data/Repositories/CollectionTagRepository.cs +++ b/API/Data/Repositories/CollectionTagRepository.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using API.DTOs; diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 842b0767e..67cd83276 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Comparators; using API.Data.Scanner; using API.DTOs; using API.DTOs.Filtering; diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs index 1eb0165bb..4489cf3bd 100644 --- a/API/Data/Repositories/SettingsRepository.cs +++ b/API/Data/Repositories/SettingsRepository.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.DTOs; +using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; using API.Interfaces.Repositories; diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index 08fffa540..b3e0a5dfd 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -1,6 +1,5 @@  using System; -using System.ComponentModel.DataAnnotations; using API.Entities.Interfaces; namespace API.Entities diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index f4f0076db..17dd6d3c3 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using API.Entities.Interfaces; -using Microsoft.EntityFrameworkCore; namespace API.Entities { diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 03445ccb2..ff1a30e34 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -3,6 +3,7 @@ using System.Linq; using API.DTOs; using API.DTOs.Reader; using API.DTOs.ReadingLists; +using API.DTOs.Settings; using API.Entities; using API.Helpers.Converters; using AutoMapper; diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index db1736b0b..445e92ddb 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using API.DTOs; +using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; using AutoMapper; diff --git a/API/Interfaces/Repositories/ISeriesRepository.cs b/API/Interfaces/Repositories/ISeriesRepository.cs index c7ac41c53..9b04d6d9f 100644 --- a/API/Interfaces/Repositories/ISeriesRepository.cs +++ b/API/Interfaces/Repositories/ISeriesRepository.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using API.Data.Scanner; using API.DTOs; diff --git a/API/Interfaces/Repositories/ISettingsRepository.cs b/API/Interfaces/Repositories/ISettingsRepository.cs index f1687743d..95178ea79 100644 --- a/API/Interfaces/Repositories/ISettingsRepository.cs +++ b/API/Interfaces/Repositories/ISettingsRepository.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using API.DTOs; +using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; diff --git a/API/Interfaces/Services/IArchiveService.cs b/API/Interfaces/Services/IArchiveService.cs index ae9bddc98..f2567341a 100644 --- a/API/Interfaces/Services/IArchiveService.cs +++ b/API/Interfaces/Services/IArchiveService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO.Compression; using System.Threading.Tasks; using API.Archive; +using API.Data.Metadata; namespace API.Interfaces.Services { @@ -12,7 +13,7 @@ namespace API.Interfaces.Services int GetNumberOfPagesFromArchive(string archivePath); string GetCoverImage(string archivePath, string fileName); bool IsValidArchive(string archivePath); - string GetSummaryInfo(string archivePath); + ComicInfo GetComicInfo(string archivePath); ArchiveLibrary CanOpen(string archivePath); bool ArchiveNeedsFlattening(ZipArchive archive); Task> CreateZipForDownload(IEnumerable files, string tempFolder); diff --git a/API/Interfaces/Services/IBookService.cs b/API/Interfaces/Services/IBookService.cs index cde2cad8e..e78669755 100644 --- a/API/Interfaces/Services/IBookService.cs +++ b/API/Interfaces/Services/IBookService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using API.Data.Metadata; using API.Parser; using VersOne.Epub; @@ -20,7 +21,7 @@ namespace API.Interfaces.Services /// Book Reference, needed for if you expect Import statements /// Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book); - string GetSummaryInfo(string filePath); + ComicInfo GetComicInfo(string filePath); ParserInfo ParseInfo(string filePath); /// /// Extracts a PDF file's pages as images to an target directory diff --git a/API/Interfaces/Services/ReaderService.cs b/API/Interfaces/Services/ReaderService.cs index f46ccd7d1..7eb2e1118 100644 --- a/API/Interfaces/Services/ReaderService.cs +++ b/API/Interfaces/Services/ReaderService.cs @@ -1,7 +1,6 @@  using System; using System.Collections.Generic; -using System.Data; using System.Linq; using System.Threading.Tasks; using API.Comparators; diff --git a/API/Program.cs b/API/Program.cs index eddc4cb4e..0587cec98 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,16 +1,10 @@ using System; -using System.Collections.Generic; -using System.Data; using System.IO; -using System.Linq; using System.Security.Cryptography; using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; using API.Data; using API.Entities; -using API.Helpers; -using API.Interfaces; using API.Services; using Kavita.Common; using Kavita.Common.EnvironmentInfo; @@ -21,8 +15,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.IO; -using NetVips; using Sentry; namespace API diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 1b68956e5..621d42322 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using System.Xml.Serialization; using API.Archive; using API.Comparators; +using API.Data.Metadata; using API.Extensions; using API.Interfaces.Services; using API.Services.Tasks; @@ -293,15 +294,13 @@ namespace API.Services return null; } - public string GetSummaryInfo(string archivePath) + public ComicInfo GetComicInfo(string archivePath) { - var summary = string.Empty; - if (!IsValidArchive(archivePath)) return summary; + if (!IsValidArchive(archivePath)) return null; - ComicInfo info = null; try { - if (!File.Exists(archivePath)) return summary; + if (!File.Exists(archivePath)) return null; var libraryHandler = CanOpen(archivePath); switch (libraryHandler) @@ -309,48 +308,55 @@ namespace API.Services case ArchiveLibrary.Default: { using var archive = ZipFile.OpenRead(archivePath); - var entry = archive.Entries.SingleOrDefault(x => !Parser.Parser.HasBlacklistedFolderInPath(x.FullName) - && Path.GetFileNameWithoutExtension(x.Name)?.ToLower() == ComicInfoFilename - && !Path.GetFileNameWithoutExtension(x.Name).StartsWith(Parser.Parser.MacOsMetadataFileStartsWith) - && Parser.Parser.IsXml(x.FullName)); + var entry = archive.Entries.SingleOrDefault(x => + !Parser.Parser.HasBlacklistedFolderInPath(x.FullName) + && Path.GetFileNameWithoutExtension(x.Name)?.ToLower() == ComicInfoFilename + && !Path.GetFileNameWithoutExtension(x.Name) + .StartsWith(Parser.Parser.MacOsMetadataFileStartsWith) + && Parser.Parser.IsXml(x.FullName)); if (entry != null) { using var stream = entry.Open(); var serializer = new XmlSerializer(typeof(ComicInfo)); - info = (ComicInfo) serializer.Deserialize(stream); + return (ComicInfo) serializer.Deserialize(stream); } + break; } case ArchiveLibrary.SharpCompress: { using var archive = ArchiveFactory.Open(archivePath); - info = FindComicInfoXml(archive.Entries.Where(entry => !entry.IsDirectory - && !Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) - && !Path.GetFileNameWithoutExtension(entry.Key).StartsWith(Parser.Parser.MacOsMetadataFileStartsWith) + return FindComicInfoXml(archive.Entries.Where(entry => !entry.IsDirectory + && !Parser.Parser + .HasBlacklistedFolderInPath( + Path.GetDirectoryName( + entry.Key) ?? string.Empty) + && !Path + .GetFileNameWithoutExtension( + entry.Key).StartsWith(Parser + .Parser + .MacOsMetadataFileStartsWith) && Parser.Parser.IsXml(entry.Key))); - break; } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[GetSummaryInfo] This archive cannot be read: {ArchivePath}", archivePath); - return summary; + _logger.LogWarning("[GetComicInfo] This archive cannot be read: {ArchivePath}", archivePath); + return null; default: - _logger.LogWarning("[GetSummaryInfo] There was an exception when reading archive stream: {ArchivePath}", archivePath); - return summary; - } - - if (info != null) - { - return info.Summary; + _logger.LogWarning( + "[GetComicInfo] There was an exception when reading archive stream: {ArchivePath}", + archivePath); + return null; } } catch (Exception ex) { - _logger.LogWarning(ex, "[GetSummaryInfo] There was an exception when reading archive stream: {Filepath}", archivePath); + _logger.LogWarning(ex, "[GetComicInfo] There was an exception when reading archive stream: {Filepath}", archivePath); } - return summary; + return null; } + private static void ExtractArchiveEntities(IEnumerable entries, string extractPath) { DirectoryService.ExistOrCreate(extractPath); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index b63a0253e..a114b3bd6 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -4,12 +4,12 @@ using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; -using System.Net; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; +using API.Data.Metadata; using API.Entities.Enums; using API.Interfaces.Services; using API.Parser; @@ -165,22 +165,43 @@ namespace API.Services return RemoveWhiteSpaceFromStylesheets(stylesheet.ToCss()); } - public string GetSummaryInfo(string filePath) + public ComicInfo GetComicInfo(string filePath) { - if (!IsValidFile(filePath) || Parser.Parser.IsPdf(filePath)) return string.Empty; - + if (!IsValidFile(filePath) || Parser.Parser.IsPdf(filePath)) return null; try { using var epubBook = EpubReader.OpenBook(filePath); - return epubBook.Schema.Package.Metadata.Description; + var publicationDate = + epubBook.Schema.Package.Metadata.Dates.FirstOrDefault(date => date.Event == "publication")?.Date; + + var info = new ComicInfo() + { + Summary = epubBook.Schema.Package.Metadata.Description, + Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators), + Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers), + Month = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Month : 0, + Year = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Year : 0, + }; + // Parse tags not exposed via Library + foreach (var metadataItem in epubBook.Schema.Package.Metadata.MetaItems) + { + switch (metadataItem.Name) + { + case "calibre:rating": + info.UserRating = float.Parse(metadataItem.Content); + break; + } + } + + return info; } catch (Exception ex) { - _logger.LogWarning(ex, "[BookService] There was an exception getting summary, defaulting to empty string"); + _logger.LogWarning(ex, "[GetComicInfo] There was an exception getting metadata"); } - return string.Empty; + return null; } private bool IsValidFile(string filePath) diff --git a/API/Services/ComicInfo.cs b/API/Services/ComicInfo.cs deleted file mode 100644 index 55e823ee4..000000000 --- a/API/Services/ComicInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace API.Services -{ - public class ComicInfo - { - public string Summary { get; set; } - public string Title { get; set; } - public string Series { get; set; } - public string Notes { get; set; } - public string Publisher { get; set; } - public string Genre { get; set; } - public int PageCount { get; set; } - // ReSharper disable once InconsistentNaming - public string LanguageISO { get; set; } - public string Web { get; set; } - } -} \ No newline at end of file diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index fddb3fffe..d17fea327 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -95,7 +95,7 @@ namespace API.Services /// File name with extension of the file. This will always write to public static string WriteCoverThumbnail(Stream stream, string fileName) { - using var thumbnail = NetVips.Image.ThumbnailStream(stream, ThumbnailWidth); + using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth); var filename = fileName + ".png"; thumbnail.WriteToFile(Path.Join(DirectoryService.CoverImageDirectory, fileName + ".png")); return filename; diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 14be8ce17..40e44f02a 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -1,10 +1,10 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using API.Comparators; +using API.Data.Metadata; using API.Data.Repositories; using API.Entities; using API.Entities.Enums; @@ -171,6 +171,9 @@ namespace API.Services private bool UpdateSeriesSummary(Series series, bool forceUpdate) { + // NOTE: This can be problematic when the file changes and a summary already exists, but it is likely + // better to let the user kick off a refresh metadata on an individual Series than having overhead of + // checking File last write time. if (!string.IsNullOrEmpty(series.Summary) && !forceUpdate) return false; var isBook = series.Library.Type == LibraryType.Book; @@ -181,16 +184,21 @@ namespace API.Services if (firstFile == null || (!forceUpdate && !firstFile.HasFileBeenModified())) return false; if (Parser.Parser.IsPdf(firstFile.FilePath)) return false; - if (series.Format is MangaFormat.Archive or MangaFormat.Epub) + var comicInfo = GetComicInfo(series.Format, firstFile); + if (string.IsNullOrEmpty(comicInfo.Summary)) return false; + + series.Summary = comicInfo.Summary; + return true; + } + + private ComicInfo GetComicInfo(MangaFormat format, MangaFile firstFile) + { + if (format is MangaFormat.Archive or MangaFormat.Epub) { - var summary = Parser.Parser.IsEpub(firstFile.FilePath) ? _bookService.GetSummaryInfo(firstFile.FilePath) : _archiveService.GetSummaryInfo(firstFile.FilePath); - if (!string.IsNullOrEmpty(series.Summary)) - { - series.Summary = summary; - return true; - } + return Parser.Parser.IsEpub(firstFile.FilePath) ? _bookService.GetComicInfo(firstFile.FilePath) : _archiveService.GetComicInfo(firstFile.FilePath); } - return false; + + return null; } diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index 35388985a..b4dc3910b 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -125,7 +125,7 @@ namespace API.Services.Tasks _directoryService.CopyFilesToDirectory( chapterImages.Select(s => Path.Join(DirectoryService.CoverImageDirectory, s)), outputTempDir); } - catch (IOException e) + catch (IOException) { // Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file. } diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index c1edf2e6b..93f8ec5db 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -1,11 +1,9 @@ using System.IO; -using System.Linq; using System.Threading.Tasks; using API.Interfaces; using API.Interfaces.Services; using Hangfire; using Microsoft.Extensions.Logging; -using NetVips; namespace API.Services.Tasks { diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index f1ba9cc59..2f16bf524 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -76,7 +76,7 @@ namespace API.Services.Tasks try { _unitOfWork.SeriesRepository.Remove(series); - await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds); + await CommitAndSend(totalFiles, parsedSeries, sw, scanElapsedTime, series); } catch (Exception ex) { @@ -121,7 +121,7 @@ namespace API.Services.Tasks try { UpdateSeries(series, parsedSeries); - await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds); + await CommitAndSend(totalFiles, parsedSeries, sw, scanElapsedTime, series); } catch (Exception ex) { @@ -131,6 +131,9 @@ namespace API.Services.Tasks // Tell UI that this series is done await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.ScanSeriesEvent(seriesId, series.Name), cancellationToken: token); + await CleanupDbEntities(); + BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); + BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, series.Id, false)); } private static void RemoveParsedInfosNotForSeries(Dictionary> parsedSeries, Series series) @@ -143,8 +146,8 @@ namespace API.Services.Tasks } } - private async Task CommitAndSend(int libraryId, int seriesId, int totalFiles, - Dictionary> parsedSeries, Stopwatch sw, long scanElapsedTime, Series series, int[] chapterIds) + private async Task CommitAndSend(int totalFiles, + Dictionary> parsedSeries, Stopwatch sw, long scanElapsedTime, Series series) { if (_unitOfWork.HasChanges()) { @@ -152,10 +155,6 @@ namespace API.Services.Tasks _logger.LogInformation( "Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}", totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name); - - await CleanupDbEntities(); - BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, false)); - BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); } } @@ -225,7 +224,7 @@ namespace API.Services.Tasks "[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan"); } - await CleanupAbandonedChapters(); + await CleanupDbEntities(); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false)); await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress, From 7c4e79ec55c84d11082d92a0724fd416d72bed8a Mon Sep 17 00:00:00 2001 From: majora2007 Date: Mon, 4 Oct 2021 23:19:32 +0000 Subject: [PATCH 02/10] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 6596dfbd8..9b195f3ee 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,7 +4,7 @@ net5.0 kavitareader.com Kavita - 0.4.6.14 + 0.4.6.15 en From 2183bf2d5905ea3e86919082d184071c20443763 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 4 Oct 2021 17:18:42 -0700 Subject: [PATCH 03/10] Misc Bugfixes (#637) * Use null coalescing for comicinfo * Removed Sentry. The integrations were not providing any userful information. * Updated to the latest versions of dependencies --- API.Tests/API.Tests.csproj | 4 +- API/API.csproj | 29 ++-- API/Program.cs | 59 -------- API/Services/MetadataService.cs | 2 +- Kavita.Common/Kavita.Common.csproj | 3 +- UI/Web/package-lock.json | 129 ------------------ UI/Web/package.json | 2 - UI/Web/src/app/_services/account.service.ts | 7 - .../src/app/_services/message-hub.service.ts | 2 +- UI/Web/src/app/app.module.ts | 55 +------- 10 files changed, 20 insertions(+), 272 deletions(-) diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index e01bab216..59ecff406 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/API/API.csproj b/API/API.csproj index d4cfe4316..01bfaa2a9 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -38,38 +38,37 @@ - + - - + + - + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/API/Program.cs b/API/Program.cs index 0587cec98..06c860366 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,13 +1,11 @@ using System; using System.IO; using System.Security.Cryptography; -using System.Threading; using System.Threading.Tasks; using API.Data; using API.Entities; using API.Services; using Kavita.Common; -using Kavita.Common.EnvironmentInfo; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -15,7 +13,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Sentry; namespace API { @@ -95,62 +92,6 @@ namespace API opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; }); }); - var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - if (environment != Environments.Development) - { - webBuilder.UseSentry(options => - { - options.Dsn = "https://40f4e7b49c094172a6f99d61efb2740f@o641015.ingest.sentry.io/5757423"; - options.MaxBreadcrumbs = 200; - options.AttachStacktrace = true; - options.Debug = false; - options.SendDefaultPii = false; - options.DiagnosticLevel = SentryLevel.Debug; - options.ShutdownTimeout = TimeSpan.FromSeconds(5); - options.Release = BuildInfo.Version.ToString(); - options.AddExceptionFilterForType(); - options.AddExceptionFilterForType(); - options.AddExceptionFilterForType(); - options.AddExceptionFilterForType(); - - options.BeforeSend = sentryEvent => - { - if (sentryEvent.Exception != null - && sentryEvent.Exception.Message.StartsWith("[GetCoverImage]") - && sentryEvent.Exception.Message.StartsWith("[BookService]") - && sentryEvent.Exception.Message.StartsWith("[ExtractArchive]") - && sentryEvent.Exception.Message.StartsWith("[GetSummaryInfo]") - && sentryEvent.Exception.Message.StartsWith("[GetSummaryInfo]") - && sentryEvent.Exception.Message.StartsWith("[GetNumberOfPagesFromArchive]") - && sentryEvent.Exception.Message.Contains("EPUB parsing error") - && sentryEvent.Exception.Message.Contains("Unsupported EPUB version") - && sentryEvent.Exception.Message.Contains("Incorrect EPUB") - && sentryEvent.Exception.Message.Contains("Access is Denied")) - { - return null; // Don't send this event to Sentry - } - - sentryEvent.ServerName = null; // Never send Server Name to Sentry - return sentryEvent; - }; - - options.ConfigureScope(scope => - { - scope.User = new User() - { - Id = HashUtil.AnonymousToken() - }; - scope.Contexts.App.Name = BuildInfo.AppName; - scope.Contexts.App.Version = BuildInfo.Version.ToString(); - scope.Contexts.App.StartTime = DateTime.UtcNow; - scope.Contexts.App.Hash = HashUtil.AnonymousToken(); - scope.Contexts.App.Build = BuildInfo.Release; - scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name); - scope.SetTag("branch", BuildInfo.Branch); - }); - }); - } - webBuilder.UseStartup(); }); } diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 40e44f02a..820b9246c 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -185,7 +185,7 @@ namespace API.Services if (Parser.Parser.IsPdf(firstFile.FilePath)) return false; var comicInfo = GetComicInfo(series.Format, firstFile); - if (string.IsNullOrEmpty(comicInfo.Summary)) return false; + if (string.IsNullOrEmpty(comicInfo?.Summary)) return false; series.Summary = comicInfo.Summary; return true; diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 9b195f3ee..a8960e4bb 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -11,8 +11,7 @@ - - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 2ae36e55d..e47a8be17 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -2679,135 +2679,6 @@ } } }, - "@sentry/angular": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-6.10.0.tgz", - "integrity": "sha512-SSnsz4sVu9LJh7RM+z9FopWytl2yYNZQ2nK/zv/6iQKIBOqvnCqUIPjVjq1rFYXOe0jOJKsn0QlQLKp4MajYMg==", - "requires": { - "@sentry/browser": "6.10.0", - "@sentry/types": "6.10.0", - "@sentry/utils": "6.10.0", - "rxjs": "^6.6.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/browser": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.10.0.tgz", - "integrity": "sha512-H0Blgp8f8bomebkkGWIgxHVjabtQAlsKJDiFXBg7gIc75YcarRxwH0R3hMog1/h8mmv4CGGUsy5ljYW6jsNnvA==", - "requires": { - "@sentry/core": "6.10.0", - "@sentry/types": "6.10.0", - "@sentry/utils": "6.10.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/core": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.10.0.tgz", - "integrity": "sha512-5KlxHJlbD7AMo+b9pMGkjxUOfMILtsqCtGgI7DMvZNfEkdohO8QgUY+hPqr540kmwArFS91ipQYWhqzGaOhM3Q==", - "requires": { - "@sentry/hub": "6.10.0", - "@sentry/minimal": "6.10.0", - "@sentry/types": "6.10.0", - "@sentry/utils": "6.10.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/hub": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.10.0.tgz", - "integrity": "sha512-MV8wjhWiFAXZAhmj7Ef5QdBr2IF93u8xXiIo2J+dRZ7eVa4/ZszoUiDbhUcl/TPxczaw4oW2a6tINBNFLzXiig==", - "requires": { - "@sentry/types": "6.10.0", - "@sentry/utils": "6.10.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/integrations": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.10.0.tgz", - "integrity": "sha512-NMtB0jjFYFZRxyjYu2dWLThk9YPIwqhi4hYywmWkbv4/ILzi5Rwnh+aqNW6yrj8qG4b9itNMh3YvEzmf0aqauw==", - "requires": { - "@sentry/types": "6.10.0", - "@sentry/utils": "6.10.0", - "localforage": "^1.8.1", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/minimal": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.10.0.tgz", - "integrity": "sha512-yarm046UgUFIBoxqnBan2+BEgaO9KZCrLzsIsmALiQvpfW92K1lHurSawl5W6SR7wCYBnNn7CPvPE/BHFdy4YA==", - "requires": { - "@sentry/hub": "6.10.0", - "@sentry/types": "6.10.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/types": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.10.0.tgz", - "integrity": "sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw==" - }, - "@sentry/utils": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.10.0.tgz", - "integrity": "sha512-F9OczOcZMFtazYVZ6LfRIe65/eOfQbiAedIKS0li4npuMz0jKYRbxrjd/U7oLiNQkPAp4/BujU4m1ZIwq6a+tg==", - "requires": { - "@sentry/types": "6.10.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, "@sinonjs/commons": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index 515c35df8..c54eee713 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -31,8 +31,6 @@ "@ng-bootstrap/ng-bootstrap": "^9.1.0", "@ngx-lite/nav-drawer": "^0.4.6", "@ngx-lite/util": "0.0.0", - "@sentry/angular": "^6.10.0", - "@sentry/integrations": "^6.10.0", "@types/file-saver": "^2.0.1", "angular-ng-autocomplete": "^2.0.5", "bootstrap": "^4.5.0", diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index 1581dafe7..d3006815a 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -5,7 +5,6 @@ import { map, takeUntil } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { Preferences } from '../_models/preferences/preferences'; import { User } from '../_models/user'; -import * as Sentry from "@sentry/angular"; import { Router } from '@angular/router'; import { MessageHubService } from './message-hub.service'; @@ -63,12 +62,6 @@ export class AccountService implements OnDestroy { user.roles = []; const roles = this.getDecodedToken(user.token).role; Array.isArray(roles) ? user.roles = roles : user.roles.push(roles); - Sentry.setContext('admin', {'admin': this.hasAdminRole(user)}); - Sentry.configureScope(scope => { - scope.setUser({ - username: user.username - }); - }); localStorage.setItem(this.userKey, JSON.stringify(user)); localStorage.setItem(this.lastLoginKey, user.username); diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index a2e682e62..dc7490d74 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -1,7 +1,6 @@ import { EventEmitter, Injectable } from '@angular/core'; import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { User } from '@sentry/angular'; import { ToastrService } from 'ngx-toastr'; import { BehaviorSubject, ReplaySubject } from 'rxjs'; import { environment } from 'src/environments/environment'; @@ -10,6 +9,7 @@ import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event'; import { ScanLibraryProgressEvent } from '../_models/events/scan-library-progress-event'; import { ScanSeriesEvent } from '../_models/events/scan-series-event'; import { SeriesAddedEvent } from '../_models/events/series-added-event'; +import { User } from '../_models/user'; export enum EVENTS { UpdateAvailable = 'UpdateAvailable', diff --git a/UI/Web/src/app/app.module.ts b/UI/Web/src/app/app.module.ts index f68ef4341..21a4c6ae4 100644 --- a/UI/Web/src/app/app.module.ts +++ b/UI/Web/src/app/app.module.ts @@ -21,13 +21,6 @@ import { AutocompleteLibModule } from 'angular-ng-autocomplete'; import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component'; import { CarouselModule } from './carousel/carousel.module'; - -import * as Sentry from '@sentry/angular'; -import { environment } from 'src/environments/environment'; -import { version } from 'package.json'; -import { Router } from '@angular/router'; -import { RewriteFrames as RewriteFramesIntegration } from '@sentry/integrations'; -import { Dedupe as DedupeIntegration } from '@sentry/integrations'; import { PersonBadgeComponent } from './person-badge/person-badge.component'; import { TypeaheadModule } from './typeahead/typeahead.module'; import { RecentlyAddedComponent } from './recently-added/recently-added.component'; @@ -38,51 +31,6 @@ import { SAVER, getSaver } from './shared/_providers/saver.provider'; import { ReadingListModule } from './reading-list/reading-list.module'; import { DashboardComponent } from './dashboard/dashboard.component'; -let sentryProviders: any[] = []; - -if (environment.production) { - Sentry.init({ - dsn: 'https://db1a1f6445994b13a6f479512aecdd48@o641015.ingest.sentry.io/5757426', - environment: environment.production ? 'prod' : 'dev', - release: version, - integrations: [ - new Sentry.Integrations.GlobalHandlers({ - onunhandledrejection: true, - onerror: true - }), - new DedupeIntegration(), - new RewriteFramesIntegration(), - ], - ignoreErrors: [new RegExp(/\/api\/admin/)], - tracesSampleRate: 0, - }); - - Sentry.configureScope(scope => { - scope.setUser({ - username: 'Not authorized' - }); - scope.setTag('production', environment.production); - scope.setTag('version', version); - }); - - sentryProviders = [{ - provide: ErrorHandler, - useValue: Sentry.createErrorHandler({ - showDialog: false, - }), - }, - { - provide: Sentry.TraceService, - deps: [Router], - }, - { - provide: APP_INITIALIZER, - useFactory: () => () => {}, - deps: [Sentry.TraceService], - multi: true, - }]; -} - @NgModule({ declarations: [ AppComponent, @@ -133,8 +81,7 @@ if (environment.production) { {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, Title, - {provide: SAVER, useFactory: getSaver}, - ...sentryProviders, + {provide: SAVER, useFactory: getSaver} ], entryComponents: [], bootstrap: [AppComponent] From 323bddb7521c5d61bd7873b14172077664be2855 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Tue, 5 Oct 2021 00:27:27 +0000 Subject: [PATCH 04/10] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index a8960e4bb..b1ea4c8a5 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,7 +4,7 @@ net5.0 kavitareader.com Kavita - 0.4.6.15 + 0.4.6.16 en From 2727dfa7b9648e211c56041a591a8de6a2ae9e81 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 5 Oct 2021 17:42:43 -0700 Subject: [PATCH 05/10] Book Polishing (#639) * Downgraded ExCSS, the new version has a regression that breaks toCss(). * Cleaned up the next/prev disabling to work more reliably. Fixed an issue with double arrow on next not disappearing after hitting last page then going back one page. * Cleaned up some css padding on the side nav progress bar * Remove the ability to save * Updated colors of nav bar and side drawer to be darker to separate them from the reader * Fixed a missed element * Removed dead code * Removed TODO, it can't be changed * Refactored bookmarking on the book progress to have pinpoint accuracy for remembering scroll position. Now will choose the top line on the page. * Fixed a style issue when applying dark mode from light mode in book reader --- API/API.csproj | 2 +- API/Services/ImageService.cs | 4 +- UI/Web/package-lock.json | 21 +-- .../book-reader/book-reader.component.html | 20 +-- .../book-reader/book-reader.component.scss | 73 ++++------- .../book-reader/book-reader.component.ts | 123 +++++++----------- .../infinite-scroller.component.ts | 2 +- .../app/shared/_services/utility.service.ts | 10 ++ 8 files changed, 91 insertions(+), 164 deletions(-) diff --git a/API/API.csproj b/API/API.csproj index 01bfaa2a9..137a3a985 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -38,7 +38,7 @@ - + diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index d17fea327..c2b3d4126 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -46,9 +46,7 @@ namespace API.Services var firstImage = _directoryService.GetFilesWithExtension(directory, Parser.Parser.ImageFileExtensions) .OrderBy(f => f, new NaturalSortComparer()).FirstOrDefault(); - - - + return firstImage; } diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index e47a8be17..c69cf5812 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -7392,7 +7392,8 @@ "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true }, "import-fresh": { "version": "2.0.0", @@ -9903,24 +9904,6 @@ "json5": "^2.1.2" } }, - "localforage": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz", - "integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==", - "requires": { - "lie": "3.1.1" - }, - "dependencies": { - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", - "requires": { - "immediate": "~3.0.5" - } - } - } - }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html index 93f458d35..f3c7ae4e0 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html @@ -58,17 +58,16 @@
- - +
-
{{pageNum}}
-
+
{{pageNum}}
+
-
{{maxPages - 1}}
+
{{maxPages - 1}}
@@ -100,15 +99,6 @@
- -
@@ -138,7 +128,7 @@ [disabled]="IsNextDisabled" (click)="nextPage()" title="{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} Page"> {{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}}  - +
diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss index 042274399..5672139f5 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss @@ -28,6 +28,7 @@ src: url(../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype"); } +@import '../../../assets/themes/dark.scss'; $primary-color: #0062cc; .control-container { @@ -42,6 +43,11 @@ $primary-color: #0062cc; } } +.page-stub { + margin-top: 6px; + padding-left: 2px; + padding-right: 2px; +} .dark-mode { @@ -73,60 +79,12 @@ $primary-color: #0062cc; color: #8db2e5 !important; } - // Coppied - // html, body { - // color: #dcdcdc !important; - // background-image: none !important; - // background-color: #292929 !important; - // } - - // html::before, body::before { - // background-image: none !important; - // } - - // html *:not(input) {color: #dcdcdc !important} - // html * {background-color: rgb(41, 41, 41, 0.90) !important} - - // html *, html *[id], html *[class] { - // box-shadow: none !important; - // text-shadow: none !important; - // border-radius: unset !important; - // border-color: #555555 !important; - // outline-color: #555555 !important; - // } - - img, img[src] { + img, img[src] { z-index: 1; filter: brightness(0.85) !important; background-color: initial !important; - } + } - // video, video[src] { - // z-index: 1; - // background-color: transparent !important; - // } - - // input:not([type='button']):not([type='submit']) { - // color: #dcdcdc !important; - // background-image: none !important; - // background-color: #333333 !important; - // } - - // textarea, textarea[class], input[type='text'], input[type='text'][class] { - // color: #dcdcdc !important; - // background-color: #555555 !important; - // } - - // svg:not([fill]) {fill: #7d7d7d !important} - // li, select {background-image: none !important} - // input[type='text'], input[type='search'] {text-indent: 10px} - // a {background-color: rgba(255, 255, 255, 0.01) !important} - // html cite, html cite *, html cite *[class] {color: #029833 !important} - // svg[fill], button, input[type='button'], input[type='submit'] {opacity: 0.85 !important} - - // :before {color: #dcdcdc !important} - // :link:not(cite), :link *:not(cite) {color: #8db2e5 !important} - // :visited, :visited *, :visited *[class] {color: rgb(211, 138, 138) !important} :visited, :visited *, :visited *[class] {color: rgb(211, 138, 138) !important} :link:not(cite), :link *:not(cite) {color: #8db2e5 !important} } @@ -136,6 +94,21 @@ $primary-color: #0062cc; overflow: hidden; } +.dark-mode { + .reading-bar, .book-title, .drawer-body, .drawer-container { + background-color: $dark-form-background-no-opacity; + } + button { + background-color: $dark-form-background-no-opacity; + } +} + +::ng-deep .dark-mode .drawer-container { + .header, body, *:not(.progress-bar) { + background-color: $dark-form-background-no-opacity !important; + } +} + @media(max-width: 875px) { .book-title { display: none; diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts index 494bf6907..33aff1719 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts @@ -14,11 +14,10 @@ import { SeriesService } from 'src/app/_services/series.service'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { BookService } from '../book.service'; -import { KEY_CODES } from 'src/app/shared/_services/utility.service'; +import { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service'; import { BookChapterItem } from '../_models/book-chapter-item'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { Stack } from 'src/app/shared/data-structures/stack'; -import { Preferences } from 'src/app/_models/preferences/preferences'; import { MemberService } from 'src/app/_services/member.service'; import { ReadingDirection } from 'src/app/_models/preferences/reading-direction'; import { ScrollService } from 'src/app/scroll.service'; @@ -166,7 +165,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { pageAnchors: {[n: string]: number } = {}; currentPageAnchor: string = ''; - intersectionObserver: IntersectionObserver = new IntersectionObserver((entries) => this.handleIntersection(entries), { threshold: [1] }); /** * Last seen progress part path */ @@ -186,10 +184,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { color: #e83e8c !important; } - // .btn-icon { - // background-color: transparent; - // } - :link, a { color: #8db2e5 !important; } @@ -205,25 +199,31 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { return ReadingDirection; } - get IsPrevDisabled() { + get IsPrevDisabled(): boolean { if (this.readingDirection === ReadingDirection.LeftToRight) { + // Acting as Previous button return this.prevPageDisabled && this.pageNum === 0; - } - return this.nextPageDisabled && this.pageNum + 1 >= this.maxPages - 1; + } else { + // Acting as a Next button + return this.nextPageDisabled && this.pageNum + 1 > this.maxPages - 1; + } } - get IsNextDisabled() { + get IsNextDisabled(): boolean { if (this.readingDirection === ReadingDirection.LeftToRight) { - this.nextPageDisabled && this.pageNum + 1 >= this.maxPages - 1; + // Acting as Next button + return this.nextPageDisabled && this.pageNum + 1 > this.maxPages - 1; + } else { + // Acting as Previous button + return this.prevPageDisabled && this.pageNum === 0; } - return this.prevPageDisabled && this.pageNum === 0; } constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private seriesService: SeriesService, private readerService: ReaderService, private location: Location, private renderer: Renderer2, private navService: NavService, private toastr: ToastrService, private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService, - private scrollService: ScrollService) { + private scrollService: ScrollService, private utilityService: UtilityService) { this.navService.hideNavBar(); this.darkModeStyleElem = this.renderer.createElement('style'); @@ -296,6 +296,36 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + + // Find the element that is on screen to bookmark against + const intersectingEntries = Array.from(this.readingSectionElemRef.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span')) + .filter(element => !element.classList.contains('no-observe')) + .filter(entry => { + return this.utilityService.isInViewport(entry, this.topOffset); + }); + + intersectingEntries.sort((a: Element, b: Element) => { + const aTop = a.getBoundingClientRect().top; + const bTop = b.getBoundingClientRect().top; + if (aTop < bTop) { + return -1; + } + if (aTop > bTop) { + return 1; + } + + return 0; + }); + + if (intersectingEntries.length > 0) { + let path = this.getXPathTo(intersectingEntries[0]); + if (path === '') { return; } + if (!path.startsWith('id')) { + path = '//html[1]/' + path; + } + this.lastSeenScrollPartPath = path; + } + if (this.lastSeenScrollPartPath !== '' && !this.incognitoMode) { this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */}); } @@ -326,7 +356,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.onDestroy.next(); this.onDestroy.complete(); - this.intersectionObserver.disconnect(); } ngOnInit(): void { @@ -443,12 +472,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - handleIntersection(entries: IntersectionObserverEntry[]) { - let intersectingEntries = Array.from(entries) - .filter(entry => entry.isIntersecting) - .map(entry => entry.target) - intersectingEntries.sort((a: Element, b: Element) => { - const aTop = a.getBoundingClientRect().top; + sortElements(a: Element, b: Element) { + const aTop = a.getBoundingClientRect().top; const bTop = b.getBoundingClientRect().top; if (aTop < bTop) { return -1; @@ -458,17 +483,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } return 0; - }); - - - if (intersectingEntries.length > 0) { - let path = this.getXPathTo(intersectingEntries[0]); - if (path === '') { return; } - if (!path.startsWith('id')) { - path = '//html[1]/' + path; - } - this.lastSeenScrollPartPath = path; - } } loadNextChapter() { @@ -541,16 +555,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - resetSettings(afterSave: boolean = false) { + resetSettings() { const windowWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; let margin = '15%'; if (windowWidth <= 700) { - if (afterSave && this.user.preferences.bookReaderMargin !== 0) { - this.toastr.info('Margin will be reset to 0% on mobile. You do not have to save for settings to take effect.'); - } margin = '0%'; } if (this.user) { @@ -558,11 +569,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { margin = this.user.preferences.bookReaderMargin + '%'; } this.pageStyles = {'font-family': this.user.preferences.bookReaderFontFamily, 'font-size': this.user.preferences.bookReaderFontSize + '%', 'margin-left': margin, 'margin-right': margin, 'line-height': this.user.preferences.bookReaderLineSpacing + '%'}; - if (!afterSave) { - if (this.user.preferences.siteDarkMode && !this.user.preferences.bookReaderDarkMode) { - this.user.preferences.bookReaderDarkMode = true; - } - } this.toggleDarkMode(this.user.preferences.bookReaderDarkMode); } else { @@ -657,12 +663,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } setupPageAnchors() { - this.readingSectionElemRef.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span').forEach(elem => { - if (!elem.classList.contains('no-observe')) { - this.intersectionObserver.observe(elem); - } - }); - this.pageAnchors = {}; this.currentPageAnchor = ''; const ids = this.chapters.map(item => item.children).flat().filter(item => item.page === this.pageNum).map(item => item.part).filter(item => item.length > 0); @@ -875,7 +875,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } getDarkModeBackgroundColor() { - return this.darkMode ? '#292929' : '#fff'; + return this.darkMode ? '#010409' : '#fff'; } setOverrideStyles() { @@ -896,33 +896,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } - saveSettings() { - if (this.user === undefined) return; - const modelSettings = this.settingsForm.value; - const data: Preferences = { - readingDirection: this.user.preferences.readingDirection, - scalingOption: this.user.preferences.scalingOption, - pageSplitOption: this.user.preferences.pageSplitOption, - autoCloseMenu: this.user.preferences.autoCloseMenu, - readerMode: this.user.preferences.readerMode, - bookReaderDarkMode: this.darkMode, - bookReaderFontFamily: modelSettings.bookReaderFontFamily, - bookReaderFontSize: parseInt(this.pageStyles['font-size'].substr(0, this.pageStyles['font-size'].length - 1), 10), - bookReaderLineSpacing: parseInt(this.pageStyles['line-height'].replace('!important', '').trim(), 10), - bookReaderMargin: parseInt(this.pageStyles['margin-left'].replace('%', '').replace('!important', '').trim(), 10), - bookReaderTapToPaginate: this.clickToPaginate, - bookReaderReadingDirection: this.readingDirection, - siteDarkMode: this.user.preferences.siteDarkMode, - }; - this.accountService.updatePreferences(data).pipe(take(1)).subscribe((updatedPrefs) => { - this.toastr.success('User settings updated'); - if (this.user) { - this.user.preferences = updatedPrefs; - } - this.resetSettings(true); - }); - } - toggleDrawer() { this.topOffset = this.stickyTopElemRef.nativeElement?.offsetHeight; this.drawerOpen = !this.drawerOpen; diff --git a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts b/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts index d40eec83d..d3978b4bf 100644 --- a/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts +++ b/UI/Web/src/app/manga-reader/infinite-scroller/infinite-scroller.component.ts @@ -205,7 +205,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { return document.documentElement.offsetHeight + document.documentElement.scrollTop; } getScrollTop() { - return document.documentElement.scrollTop + return document.documentElement.scrollTop; } checkIfShouldTriggerContinuousReader() { diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index b66f67cb9..23fb459f8 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -127,4 +127,14 @@ export class UtilityService { return Breakpoint.Desktop; } + isInViewport(element: Element, additionalTopOffset: number = 0) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= additionalTopOffset && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + } + } From ef0cf257d9c0c91c406ffb2802a3c6a9b16b2714 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Wed, 6 Oct 2021 00:51:43 +0000 Subject: [PATCH 06/10] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index b1ea4c8a5..c7bed3c15 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,7 +4,7 @@ net5.0 kavitareader.com Kavita - 0.4.6.16 + 0.4.6.17 en From 6732a04473056c6f9014da557a23cbcf58b2a3b0 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 5 Oct 2021 18:23:02 -0700 Subject: [PATCH 07/10] Book Polishing (#640) * Fixed an issue with build due to failed budget size --- .../src/app/book-reader/book-reader/book-reader.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss index 5672139f5..9bec2de98 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss @@ -28,7 +28,7 @@ src: url(../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype"); } -@import '../../../assets/themes/dark.scss'; +$dark-form-background-no-opacity: rgb(1, 4, 9); $primary-color: #0062cc; .control-container { From 56239ee0a71df55a555642177cd2fbbbeff29274 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Wed, 6 Oct 2021 01:31:31 +0000 Subject: [PATCH 08/10] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index c7bed3c15..933136b96 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,7 +4,7 @@ net5.0 kavitareader.com Kavita - 0.4.6.17 + 0.4.6.18 en From 27aaa040c4e2c498276fe6c47228471ac2ef5104 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 6 Oct 2021 05:42:02 -0700 Subject: [PATCH 09/10] Base Url Support (#642) * Added base url config * UI side is not working * Working base url more * Attempt to get UI to work with base url * Implemented the ability to set the Base URL for the app * Hooked in Base URL as a managed setting * Ensure we always start with / for base url * Removed default base href from debug builds. Cleaned up an issue with base url migration. * Fixed an issue with our BaseURL migration --- API/Controllers/SettingsController.cs | 11 ++++ API/DTOs/Settings/ServerSettingDTO.cs | 4 ++ API/Data/Seed.cs | 10 ++++ API/Entities/Enums/ServerSettingKey.cs | 4 +- .../Converters/ServerSettingConverter.cs | 3 + API/Startup.cs | 12 ++++ API/appsettings.Development.json | 4 +- Kavita.Common/Configuration.cs | 57 ++++++++++++++++++- .../src/app/admin/_models/server-settings.ts | 1 + .../manage-library.component.ts | 2 +- .../manage-settings.component.html | 9 ++- .../manage-settings.component.ts | 2 + UI/Web/src/app/app.module.ts | 12 ++-- UI/Web/src/index.html | 5 ++ 14 files changed, 126 insertions(+), 10 deletions(-) diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index d3075de1a..a7896a568 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -85,6 +85,17 @@ namespace API.Controllers _unitOfWork.SettingsRepository.Update(setting); } + if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value) + { + var path = !updateSettingsDto.BaseUrl.StartsWith("/") + ? $"/{updateSettingsDto.BaseUrl}" + : updateSettingsDto.BaseUrl; + setting.Value = path; + // BaseUrl is managed in appSetting.json + Configuration.BaseUrl = updateSettingsDto.BaseUrl; + _unitOfWork.SettingsRepository.Update(setting); + } + if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value) { setting.Value = updateSettingsDto.LoggingLevel + string.Empty; diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 0876fbfa0..957270b8b 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -26,5 +26,9 @@ /// Enables Authentication on the server. Defaults to true. ///
public bool EnableAuthentication { get; set; } + /// + /// Base Url for the kavita. Defaults to "/". Managed in appsettings.json.Requires restart to take effect. + /// + public string BaseUrl { get; set; } = "/"; } } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 7bc438ab5..f1b9d6cc5 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -50,6 +50,7 @@ namespace API.Data new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, new () {Key = ServerSettingKey.EnableOpds, Value = "false"}, new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"}, + new () {Key = ServerSettingKey.BaseUrl, Value = ""},// Not used from DB, but DB is sync with appSettings.json }; foreach (var defaultSetting in defaultSettings) @@ -63,11 +64,20 @@ namespace API.Data await context.SaveChangesAsync(); + if (string.IsNullOrEmpty(Configuration.BaseUrl)) + { + Configuration.BaseUrl = "/"; + } + // Port and LoggingLevel are managed in appSettings.json. Update the DB values to match context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value = Configuration.Port + string.Empty; context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value = Configuration.LogLevel + string.Empty; + context.ServerSetting.First(s => s.Key == ServerSettingKey.BaseUrl).Value = + Configuration.BaseUrl; + + await context.SaveChangesAsync(); diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index cbf68f013..997d0a33e 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -21,7 +21,9 @@ namespace API.Entities.Enums [Description("EnableOpds")] EnableOpds = 7, [Description("EnableAuthentication")] - EnableAuthentication = 8 + EnableAuthentication = 8, + [Description("BaseUrl")] + BaseUrl = 9 } } diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index 445e92ddb..86ed6235e 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -39,6 +39,9 @@ namespace API.Helpers.Converters case ServerSettingKey.EnableAuthentication: destination.EnableAuthentication = bool.Parse(row.Value); break; + case ServerSettingKey.BaseUrl: + destination.BaseUrl = row.Value; + break; } } diff --git a/API/Startup.cs b/API/Startup.cs index 3e6e5c659..ee309d537 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -160,11 +160,23 @@ namespace API app.UseDefaultFiles(); + if (!string.IsNullOrEmpty(Configuration.BaseUrl)) + { + var path = !Configuration.BaseUrl.StartsWith("/") + ? $"/{Configuration.BaseUrl}" + : Configuration.BaseUrl; + app.UsePathBase(path); + Console.WriteLine("Starting with base url as " + path); + } + app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = new FileExtensionContentTypeProvider() }); + + + app.Use(async (context, next) => { context.Response.GetTypedHeaders().CacheControl = diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index b5dc22df1..14d557be3 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -18,5 +18,7 @@ "MaxRollingFiles": 5 } }, - "Port": 5000 + "Port": 5000, + "BaseUrl": "/" + } diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index c2967c883..0c5fdfc95 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -8,7 +8,7 @@ namespace Kavita.Common { public static class Configuration { - private static readonly string AppSettingsFilename = GetAppSettingFilename(); + private static string AppSettingsFilename = GetAppSettingFilename(); public static string Branch { get => GetBranch(GetAppSettingFilename()); @@ -33,6 +33,12 @@ namespace Kavita.Common set => SetLogLevel(GetAppSettingFilename(), value); } + public static string BaseUrl + { + get => GetBaseUrl(GetAppSettingFilename()); + set => SetBaseUrl(GetAppSettingFilename(), value); + } + private static string GetAppSettingFilename() { if (!string.IsNullOrEmpty(AppSettingsFilename)) @@ -151,6 +157,55 @@ namespace Kavita.Common #endregion + #region BaseUrl + private static string GetBaseUrl(string filePath) + { + if (new OsInfo(Array.Empty()).IsDocker) + { + return "/"; + } + + try + { + var json = File.ReadAllText(filePath); + var jsonObj = JsonSerializer.Deserialize(json); + const string key = "BaseUrl"; + + if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) + { + return tokenElement.GetString(); + } + } + catch (Exception ex) + { + Console.WriteLine("Error reading app settings: " + ex.Message); + } + + return "/"; + } + + private static void SetBaseUrl(string filePath, string value) + { + if (new OsInfo(Array.Empty()).IsDocker) + { + return; + } + + var currentBaseUrl = GetBaseUrl(filePath); + var json = File.ReadAllText(filePath); + if (!json.Contains("BaseUrl")) + { + var lastBracket = json.LastIndexOf("}", StringComparison.Ordinal) - 1; + json = (json.Substring(0, lastBracket) + (",\n \"BaseUrl\": " + currentBaseUrl) + "}"); + } + else + { + json = json.Replace("\"BaseUrl\": " + currentBaseUrl, "\"BaseUrl\": " + value); + } + File.WriteAllText(filePath, json); + } + #endregion + #region LogLevel private static void SetLogLevel(string filePath, string logLevel) diff --git a/UI/Web/src/app/admin/_models/server-settings.ts b/UI/Web/src/app/admin/_models/server-settings.ts index f21b886d1..fbcb2a0f0 100644 --- a/UI/Web/src/app/admin/_models/server-settings.ts +++ b/UI/Web/src/app/admin/_models/server-settings.ts @@ -7,4 +7,5 @@ export interface ServerSettings { allowStatCollection: boolean; enableOpds: boolean; enableAuthentication: boolean; + baseUrl: string; } diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index 7160bd758..904c6f584 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { Subject } from 'rxjs'; -import { take, takeUntil, takeWhile } from 'rxjs/operators'; +import { take, takeUntil } from 'rxjs/operators'; import { ConfirmService } from 'src/app/shared/confirm.service'; import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event'; import { Library, LibraryType } from 'src/app/_models/library'; diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index eb7cc14ac..4467bcb6d 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -1,6 +1,6 @@
-

Port and Logging Level require a manual restart of Kavita to take effect.

+

Port, Base Url, and Logging Level require a manual restart of Kavita to take effect.

  Where the server place temporary files when reading. This will be cleaned up on a regular basis. @@ -8,6 +8,13 @@
+
+   + Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita + Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita + +
+
  Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect. diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index ab5a71c0c..c05464695 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -37,6 +37,7 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required])); this.settingsForm.addControl('enableOpds', new FormControl(this.serverSettings.enableOpds, [Validators.required])); this.settingsForm.addControl('enableAuthentication', new FormControl(this.serverSettings.enableAuthentication, [Validators.required])); + this.settingsForm.addControl('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.required])); }); } @@ -49,6 +50,7 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.get('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection); this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds); this.settingsForm.get('enableAuthentication')?.setValue(this.serverSettings.enableAuthentication); + this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl); } async saveSettings() { diff --git a/UI/Web/src/app/app.module.ts b/UI/Web/src/app/app.module.ts index 21a4c6ae4..76eb54f6e 100644 --- a/UI/Web/src/app/app.module.ts +++ b/UI/Web/src/app/app.module.ts @@ -1,11 +1,12 @@ import { BrowserModule, Title } from '@angular/platform-browser'; import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core'; +import { APP_BASE_HREF } from '@angular/common'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap'; import { NavHeaderComponent } from './nav-header/nav-header.component'; import { JwtInterceptor } from './_interceptors/jwt.interceptor'; @@ -24,12 +25,12 @@ import { CarouselModule } from './carousel/carousel.module'; import { PersonBadgeComponent } from './person-badge/person-badge.component'; import { TypeaheadModule } from './typeahead/typeahead.module'; import { RecentlyAddedComponent } from './recently-added/recently-added.component'; +import { InProgressComponent } from './in-progress/in-progress.component'; +import { DashboardComponent } from './dashboard/dashboard.component'; import { CardsModule } from './cards/cards.module'; import { CollectionsModule } from './collections/collections.module'; -import { InProgressComponent } from './in-progress/in-progress.component'; -import { SAVER, getSaver } from './shared/_providers/saver.provider'; import { ReadingListModule } from './reading-list/reading-list.module'; -import { DashboardComponent } from './dashboard/dashboard.component'; +import { SAVER, getSaver } from './shared/_providers/saver.provider'; @NgModule({ declarations: [ @@ -81,7 +82,8 @@ import { DashboardComponent } from './dashboard/dashboard.component'; {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, Title, - {provide: SAVER, useFactory: getSaver} + {provide: SAVER, useFactory: getSaver}, + { provide: APP_BASE_HREF, useValue: window['_app_base' as keyof Window] || '/' }, ], entryComponents: [], bootstrap: [AppComponent] diff --git a/UI/Web/src/index.html b/UI/Web/src/index.html index 0b9f60c62..834629c97 100644 --- a/UI/Web/src/index.html +++ b/UI/Web/src/index.html @@ -40,4 +40,9 @@ + From e8e838d1255f2b1b593e63d0905cb5d308019663 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Wed, 6 Oct 2021 12:50:50 +0000 Subject: [PATCH 10/10] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 933136b96..d7aa70fe2 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,7 +4,7 @@ net5.0 kavitareader.com Kavita - 0.4.6.18 + 0.4.6.19 en