From 49d1021049f84fcfcb2ae237baf7e108e47e98ab Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 21 Oct 2021 12:51:14 -0700 Subject: [PATCH] Cover Image Picking + Forwarding Headers with EPUBs (#700) * Ensure Kavita knows about forwarding headers (fixes issue with epub urls not going through https with reverse proxy). Fixed a case where cover image selection preferred nested folders vs files in root directory. * Fixed broken unit test * Added bug that I fixed to the unit tests --- .../Comparers/NaturalSortComparerTest.cs | 42 +++++++++---------- API.Tests/Services/ArchiveServiceTests.cs | 3 +- API/Comparators/NaturalSortComparer.cs | 7 +++- API/Services/ArchiveService.cs | 26 ++++++++---- API/Startup.cs | 5 ++- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/API.Tests/Comparers/NaturalSortComparerTest.cs b/API.Tests/Comparers/NaturalSortComparerTest.cs index d7c58d45a..b624caac8 100644 --- a/API.Tests/Comparers/NaturalSortComparerTest.cs +++ b/API.Tests/Comparers/NaturalSortComparerTest.cs @@ -8,42 +8,42 @@ namespace API.Tests.Comparers public class NaturalSortComparerTest { private readonly NaturalSortComparer _nc = new NaturalSortComparer(); - + [Theory] [InlineData( - new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, + new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"} )] [InlineData( - new[] {"Beelzebub_153b_RHS.zip", "Beelzebub_01_[Noodles].zip",}, + new[] {"Beelzebub_153b_RHS.zip", "Beelzebub_01_[Noodles].zip",}, new[] {"Beelzebub_01_[Noodles].zip", "Beelzebub_153b_RHS.zip"} )] [InlineData( - new[] {"[SCX-Scans]_Vandread_v02_Act02.zip", "[SCX-Scans]_Vandread_v02_Act01.zip",}, + new[] {"[SCX-Scans]_Vandread_v02_Act02.zip", "[SCX-Scans]_Vandread_v02_Act01.zip",}, new[] {"[SCX-Scans]_Vandread_v02_Act01.zip", "[SCX-Scans]_Vandread_v02_Act02.zip",} )] [InlineData( - new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",}, + new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",}, new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",} )] [InlineData( - new[] {"001.jpg", "10.jpg",}, + new[] {"001.jpg", "10.jpg",}, new[] {"001.jpg", "10.jpg",} )] [InlineData( - new[] {"10/001.jpg", "10.jpg",}, + new[] {"10/001.jpg", "10.jpg",}, new[] {"10.jpg", "10/001.jpg",} )] [InlineData( - new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"}, + new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"}, new[] {"Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #04.cbr"} )] [InlineData( - new[] {"3and4.cbz", "The World God Only Knows - Oneshot.cbz", "5.cbz", "1and2.cbz"}, + new[] {"3and4.cbz", "The World God Only Knows - Oneshot.cbz", "5.cbz", "1and2.cbz"}, new[] {"1and2.cbz", "3and4.cbz", "5.cbz", "The World God Only Knows - Oneshot.cbz"} )] [InlineData( - new[] {"Solo Leveling - c000 (v01) - p000 [Cover] [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p001 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p002 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p003 [dig] [Yen Press] [LuCaZ].jpg"}, + new[] {"Solo Leveling - c000 (v01) - p000 [Cover] [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p001 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p002 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p003 [dig] [Yen Press] [LuCaZ].jpg"}, new[] {"Solo Leveling - c000 (v01) - p000 [Cover] [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p001 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p002 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p003 [dig] [Yen Press] [LuCaZ].jpg"} )] public void TestNaturalSortComparer(string[] input, string[] expected) @@ -57,39 +57,39 @@ namespace API.Tests.Comparers i++; } } - - + + [Theory] [InlineData( - new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, + new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"} )] [InlineData( - new[] {"x2.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, + new[] {"x2.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, new[] {"x2.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"} )] [InlineData( - new[] {"Beelzebub_153b_RHS.zip", "Beelzebub_01_[Noodles].zip",}, + new[] {"Beelzebub_153b_RHS.zip", "Beelzebub_01_[Noodles].zip",}, new[] {"Beelzebub_01_[Noodles].zip", "Beelzebub_153b_RHS.zip"} )] [InlineData( - new[] {"[SCX-Scans]_Vandread_v02_Act02.zip", "[SCX-Scans]_Vandread_v02_Act01.zip","[SCX-Scans]_Vandread_v02_Act07.zip",}, + new[] {"[SCX-Scans]_Vandread_v02_Act02.zip", "[SCX-Scans]_Vandread_v02_Act01.zip","[SCX-Scans]_Vandread_v02_Act07.zip",}, new[] {"[SCX-Scans]_Vandread_v02_Act01.zip", "[SCX-Scans]_Vandread_v02_Act02.zip","[SCX-Scans]_Vandread_v02_Act07.zip",} )] [InlineData( - new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",}, + new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",}, new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",} )] [InlineData( - new[] {"001.jpg", "10.jpg",}, + new[] {"001.jpg", "10.jpg",}, new[] {"001.jpg", "10.jpg",} )] [InlineData( - new[] {"10/001.jpg", "10.jpg",}, + new[] {"10/001.jpg", "10.jpg",}, new[] {"10.jpg", "10/001.jpg",} )] [InlineData( - new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"}, + new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"}, new[] {"Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #04.cbr"} )] public void TestNaturalSortComparerLinq(string[] input, string[] expected) @@ -104,4 +104,4 @@ namespace API.Tests.Comparers } } } -} \ No newline at end of file +} diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index fc3e21dd4..7bdc18f1d 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -140,9 +140,10 @@ namespace API.Tests.Services [InlineData(new [] {"page 2.jpg", "page 10.jpg"}, "page 2.jpg")] [InlineData(new [] {"__MACOSX/cover.jpg", "vol1/page 01.jpg"}, "vol1/page 01.jpg")] [InlineData(new [] {"Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c060 (v10) - p200 [Digital] [LuCaZ].jpg", "folder.jpg"}, "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg")] + [InlineData(new [] {"001.jpg", "001 - chapter 1/001.jpg"}, "001.jpg")] public void FindFirstEntry(string[] files, string expected) { - var foundFile = _archiveService.FirstFileEntry(files); + var foundFile = ArchiveService.FirstFileEntry(files, string.Empty); Assert.Equal(expected, string.IsNullOrEmpty(foundFile) ? "" : foundFile); } diff --git a/API/Comparators/NaturalSortComparer.cs b/API/Comparators/NaturalSortComparer.cs index 9bf79db81..8fb0a74a5 100644 --- a/API/Comparators/NaturalSortComparer.cs +++ b/API/Comparators/NaturalSortComparer.cs @@ -6,6 +6,10 @@ using static System.String; namespace API.Comparators { + /// + /// Attempts to emulate Windows explorer sorting + /// + /// This is not thread-safe public sealed class NaturalSortComparer : IComparer, IDisposable { private readonly bool _isAscending; @@ -23,7 +27,6 @@ namespace API.Comparators { if (x == y) return 0; - // Should be fixed: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct. if (!_table.TryGetValue(x ?? Empty, out var x1)) { x1 = Regex.Split(x ?? Empty, "([0-9]+)"); @@ -33,7 +36,6 @@ namespace API.Comparators if (!_table.TryGetValue(y ?? Empty, out var y1)) { y1 = Regex.Split(y ?? Empty, "([0-9]+)"); - // Should be fixed: EXCEPTION: An item with the same key has already been added. Key: M:\Girls of the Wild's\Girls of the Wild's - Ep. 083 (Season 1) [LINE Webtoon].cbz _table.Add(y ?? Empty, y1); } @@ -59,6 +61,7 @@ namespace API.Comparators returnVal = 0; } + return _isAscending ? returnVal : -returnVal; } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 621d42322..10d45f232 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -123,12 +123,24 @@ namespace API.Services /// /// /// Entry name of match, null if no match - public string FirstFileEntry(IEnumerable entryFullNames) + public static string FirstFileEntry(IEnumerable entryFullNames, string archiveName) { - var result = entryFullNames.OrderBy(Path.GetFileName, new NaturalSortComparer()) - .FirstOrDefault(x => !Parser.Parser.HasBlacklistedFolderInPath(x) - && Parser.Parser.IsImage(x) - && !x.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)); + // First check if there are any files that are not in a nested folder before just comparing by filename. This is needed + // because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg. + var fullNames = entryFullNames.Where(x =>!Parser.Parser.HasBlacklistedFolderInPath(x) + && Parser.Parser.IsImage(x) + && !x.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)).ToList(); + if (fullNames.Count == 0) return null; + + var nonNestedFile = fullNames.Where(entry => (Path.GetDirectoryName(entry) ?? string.Empty).Equals(archiveName)) + .OrderBy(Path.GetFullPath, new NaturalSortComparer()) + .FirstOrDefault(); + + if (!string.IsNullOrEmpty(nonNestedFile)) return nonNestedFile; + + var result = fullNames + .OrderBy(Path.GetFileName, new NaturalSortComparer()) + .FirstOrDefault(); return string.IsNullOrEmpty(result) ? null : result; } @@ -158,7 +170,7 @@ namespace API.Services using var archive = ZipFile.OpenRead(archivePath); var entryNames = archive.Entries.Select(e => e.FullName).ToArray(); - var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames); + var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath)); var entry = archive.Entries.Single(e => e.FullName == entryName); using var stream = entry.Open(); @@ -169,7 +181,7 @@ namespace API.Services using var archive = ArchiveFactory.Open(archivePath); var entryNames = archive.Entries.Where(archiveEntry => !archiveEntry.IsDirectory).Select(e => e.Key).ToList(); - var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames); + var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath)); var entry = archive.Entries.Single(e => e.Key == entryName); using var stream = entry.OpenEntryStream(); diff --git a/API/Startup.cs b/API/Startup.cs index d0577d06d..da8f24582 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -138,7 +138,10 @@ namespace API app.UseResponseCompression(); - app.UseForwardedHeaders(); + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto + }); app.UseRouting();