More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes
This commit is contained in:
Joe Milazzo 2023-05-19 11:32:24 -05:00 committed by GitHub
parent 5f607b3dab
commit 64666540cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 131 additions and 307 deletions

View File

@ -7,11 +7,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Moq" Version="4.18.4" /> <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NSubstitute" Version="4.4.0" /> <PackageReference Include="NSubstitute" Version="4.4.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.26" /> <PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.29" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.26" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.29" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -39,4 +39,5 @@ public class BookParserTests
// var actual = API.Parser.Parser.CssImportUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3"); // var actual = API.Parser.Parser.CssImportUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3");
// Assert.Equal(expected, actual); // Assert.Equal(expected, actual);
// } // }
} }

View File

@ -60,8 +60,8 @@
<PackageReference Include="ExCSS" Version="4.1.0" /> <PackageReference Include="ExCSS" Version="4.1.0" />
<PackageReference Include="Flurl" Version="3.0.7" /> <PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="Flurl.Http" Version="3.2.4" /> <PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hangfire" Version="1.8.0" /> <PackageReference Include="Hangfire" Version="1.8.1" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.0" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.1" />
<PackageReference Include="Hangfire.InMemory" Version="0.4.0" /> <PackageReference Include="Hangfire.InMemory" Version="0.4.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" /> <PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" /> <PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
@ -102,9 +102,9 @@
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" /> <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.30.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.30.1" />
<PackageReference Include="System.IO.Abstractions" Version="19.2.26" /> <PackageReference Include="System.IO.Abstractions" Version="19.2.29" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" /> <PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="VersOne.Epub" Version="3.3.0" /> <PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -98,9 +98,10 @@ public class BookController : BaseApiController
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions); using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
var key = BookService.CoalesceKeyForAnyFile(book, file); var key = BookService.CoalesceKeyForAnyFile(book, file);
if (!book.Content.AllFiles.Local.ContainsKey(key)) return BadRequest("File was not found in book");
var bookFile = book.Content.AllFiles.Local[key]; if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest("File was not found in book");
var bookFile = book.Content.AllFiles.GetLocalFileRefByKey(key);
var content = await bookFile.ReadContentAsBytesAsync(); var content = await bookFile.ReadContentAsBytesAsync();
var contentType = BookService.GetContentType(bookFile.ContentType); var contentType = BookService.GetContentType(bookFile.ContentType);

View File

@ -117,7 +117,7 @@ public class DownloadController : BaseApiController
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files) private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
{ {
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files); var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
return PhysicalFile(zipFile, contentType, System.Web.HttpUtility.UrlEncode(fileDownloadName), true); return PhysicalFile(zipFile, contentType, Uri.EscapeDataString(fileDownloadName), true);
} }
/// <summary> /// <summary>
@ -163,7 +163,7 @@ public class DownloadController : BaseApiController
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(), MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended")); Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
return PhysicalFile(filePath, DefaultContentType, System.Web.HttpUtility.UrlEncode(downloadName), true); return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -184,7 +184,7 @@ public class SettingsController : BaseApiController
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value) if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
{ {
if (OsInfo.IsDocker) break; if (OsInfo.IsDocker) continue;
setting.Value = updateSettingsDto.Port + string.Empty; setting.Value = updateSettingsDto.Port + string.Empty;
// Port is managed in appSetting.json // Port is managed in appSetting.json
Configuration.Port = updateSettingsDto.Port; Configuration.Port = updateSettingsDto.Port;
@ -193,7 +193,7 @@ public class SettingsController : BaseApiController
if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value) if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value)
{ {
if (OsInfo.IsDocker) break; if (OsInfo.IsDocker) continue;
// Validate IP addresses // Validate IP addresses
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',')) foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(','))
{ {
@ -217,14 +217,7 @@ public class SettingsController : BaseApiController
? $"{path}/" ? $"{path}/"
: path; : path;
setting.Value = path; setting.Value = path;
try Configuration.BaseUrl = updateSettingsDto.BaseUrl;
{
Configuration.BaseUrl = updateSettingsDto.BaseUrl;
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not set base url. Give this exception to majora2007");
}
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }

View File

@ -127,7 +127,7 @@ public class BookService : IBookService
var hrefParts = CleanContentKeys(anchor.GetAttributeValue("href", string.Empty)) var hrefParts = CleanContentKeys(anchor.GetAttributeValue("href", string.Empty))
.Split("#"); .Split("#");
// Some keys get uri encoded when parsed, so replace any of those characters with original // Some keys get uri encoded when parsed, so replace any of those characters with original
var mappingKey = HttpUtility.UrlDecode(hrefParts[0]); var mappingKey = Uri.UnescapeDataString(hrefParts[0]);
if (!mappings.ContainsKey(mappingKey)) if (!mappings.ContainsKey(mappingKey))
{ {
@ -136,6 +136,15 @@ public class BookService : IBookService
var part = hrefParts.Length > 1 var part = hrefParts.Length > 1
? hrefParts[1] ? hrefParts[1]
: anchor.GetAttributeValue("href", string.Empty); : anchor.GetAttributeValue("href", string.Empty);
// hrefParts[0] might not have path from mappings
var pageKey = mappings.Keys.FirstOrDefault(mKey => mKey.EndsWith(hrefParts[0]));
if (!string.IsNullOrEmpty(pageKey))
{
mappings.TryGetValue(pageKey, out currentPage);
}
anchor.Attributes.Add("kavita-page", $"{currentPage}"); anchor.Attributes.Add("kavita-page", $"{currentPage}");
anchor.Attributes.Add("kavita-part", part); anchor.Attributes.Add("kavita-part", part);
anchor.Attributes.Remove("href"); anchor.Attributes.Remove("href");
@ -186,9 +195,9 @@ public class BookService : IBookService
{ {
key = prepend + key; key = prepend + key;
} }
if (!book.Content.AllFiles.Local.ContainsKey(key)) continue; if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile)) continue;
var bookFile = book.Content.AllFiles.Local[key]; //var bookFile = book.Content.AllFiles.Local[key];
var content = await bookFile.ReadContentAsBytesAsync(); var content = await bookFile.ReadContentAsBytesAsync();
importBuilder.Append(Encoding.UTF8.GetString(content)); importBuilder.Append(Encoding.UTF8.GetString(content));
} }
@ -227,7 +236,6 @@ public class BookService : IBookService
private static void EscapeCssImportReferences(ref string stylesheetHtml, string apiBase, string prepend) private static void EscapeCssImportReferences(ref string stylesheetHtml, string apiBase, string prepend)
{ {
//foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex().Matches(stylesheetHtml))
foreach (Match match in Parser.CssImportUrlRegex.Matches(stylesheetHtml)) foreach (Match match in Parser.CssImportUrlRegex.Matches(stylesheetHtml))
{ {
if (!match.Success) continue; if (!match.Success) continue;
@ -238,7 +246,6 @@ public class BookService : IBookService
private static void EscapeFontFamilyReferences(ref string stylesheetHtml, string apiBase, string prepend) private static void EscapeFontFamilyReferences(ref string stylesheetHtml, string apiBase, string prepend)
{ {
//foreach (Match match in Tasks.Scanner.Parser.Parser.FontSrcUrlRegex().Matches(stylesheetHtml))
foreach (Match match in Parser.FontSrcUrlRegex.Matches(stylesheetHtml)) foreach (Match match in Parser.FontSrcUrlRegex.Matches(stylesheetHtml))
{ {
if (!match.Success) continue; if (!match.Success) continue;
@ -249,7 +256,6 @@ public class BookService : IBookService
private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book) private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book)
{ {
//var matches = Tasks.Scanner.Parser.Parser.CssImageUrlRegex().Matches(stylesheetHtml);
var matches = Parser.CssImageUrlRegex.Matches(stylesheetHtml); var matches = Parser.CssImageUrlRegex.Matches(stylesheetHtml);
foreach (Match match in matches) foreach (Match match in matches)
{ {
@ -257,7 +263,7 @@ public class BookService : IBookService
var importFile = match.Groups["Filename"].Value; var importFile = match.Groups["Filename"].Value;
var key = CleanContentKeys(importFile); var key = CleanContentKeys(importFile);
if (!book.Content.AllFiles.Local.ContainsKey(key)) continue; if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) continue;
stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + key); stylesheetHtml = stylesheetHtml.Replace(importFile, apiBase + key);
} }
@ -290,7 +296,7 @@ public class BookService : IBookService
var imageFile = GetKeyForImage(book, image.Attributes[key].Value); var imageFile = GetKeyForImage(book, image.Attributes[key].Value);
image.Attributes.Remove(key); image.Attributes.Remove(key);
// UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx // UrlEncode here to transform ../ into an escaped version, which avoids blocking on nginx
image.Attributes.Add(key, $"{apiBase}" + HttpUtility.UrlEncode(imageFile)); image.Attributes.Add(key, $"{apiBase}" + Uri.EscapeDataString(imageFile));
// Add a custom class that the reader uses to ensure images stay within reader // Add a custom class that the reader uses to ensure images stay within reader
parent.AddClass("kavita-scale-width-container"); parent.AddClass("kavita-scale-width-container");
@ -307,9 +313,9 @@ public class BookService : IBookService
/// <returns></returns> /// <returns></returns>
private static string GetKeyForImage(EpubBookRef book, string imageFile) private static string GetKeyForImage(EpubBookRef book, string imageFile)
{ {
if (book.Content.Images.Local.ContainsKey(imageFile)) return imageFile; if (book.Content.Images.ContainsLocalFileRefWithKey(imageFile)) return imageFile;
var correctedKey = book.Content.Images.Local.Keys.SingleOrDefault(s => s.EndsWith(imageFile)); var correctedKey = book.Content.Images.Local.Select(s => s.Key).SingleOrDefault(s => s.EndsWith(imageFile));
if (correctedKey != null) if (correctedKey != null)
{ {
imageFile = correctedKey; imageFile = correctedKey;
@ -318,13 +324,14 @@ public class BookService : IBookService
{ {
// There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg // There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg
correctedKey = correctedKey =
book.Content.Images.Local.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty))); book.Content.Images.Local.Select(s => s.Key).SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
if (correctedKey != null) if (correctedKey != null)
{ {
imageFile = correctedKey; imageFile = correctedKey;
} }
} }
return imageFile; return imageFile;
} }
@ -342,6 +349,7 @@ public class BookService : IBookService
} }
private static void RewriteAnchors(int page, HtmlDocument doc, Dictionary<string, int> mappings) private static void RewriteAnchors(int page, HtmlDocument doc, Dictionary<string, int> mappings)
{ {
var anchors = doc.DocumentNode.SelectNodes("//a"); var anchors = doc.DocumentNode.SelectNodes("//a");
if (anchors == null) return; if (anchors == null) return;
@ -372,9 +380,9 @@ public class BookService : IBookService
var key = CleanContentKeys(styleLinks.Attributes["href"].Value); var key = CleanContentKeys(styleLinks.Attributes["href"].Value);
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml // Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
// In this case, we will do a search for the key that ends with // In this case, we will do a search for the key that ends with
if (!book.Content.Css.Local.ContainsKey(key)) if (!book.Content.Css.ContainsLocalFileRefWithKey(key))
{ {
var correctedKey = book.Content.Css.Local.Keys.SingleOrDefault(s => s.EndsWith(key)); var correctedKey = book.Content.Css.Local.Select(s => s.Key).SingleOrDefault(s => s.EndsWith(key));
if (correctedKey == null) if (correctedKey == null)
{ {
_logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key); _logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key);
@ -386,7 +394,7 @@ public class BookService : IBookService
try try
{ {
var cssFile = book.Content.Css.Local[key]; var cssFile = book.Content.Css.GetLocalFileRefByKey(key);
var styleContent = await ScopeStyles(await cssFile.ReadContentAsync(), apiBase, var styleContent = await ScopeStyles(await cssFile.ReadContentAsync(), apiBase,
cssFile.FilePath, book); cssFile.FilePath, book);
@ -507,6 +515,7 @@ public class BookService : IBookService
item.Property == "display-seq" && item.Refines == metadataItem.Refines); item.Property == "display-seq" && item.Refines == metadataItem.Refines);
if (count == null || count.Content == "0") if (count == null || count.Content == "0")
{ {
// TODO: Rewrite this to use a StringBuilder
// Treat this as a Collection // Treat this as a Collection
info.SeriesGroup += (string.IsNullOrEmpty(info.StoryArc) ? string.Empty : ",") + readingListElem.Title.Replace(",", "_"); info.SeriesGroup += (string.IsNullOrEmpty(info.StoryArc) ? string.Empty : ",") + readingListElem.Title.Replace(",", "_");
} }
@ -687,7 +696,9 @@ public class BookService : IBookService
foreach (var contentFileRef in await book.GetReadingOrderAsync()) foreach (var contentFileRef in await book.GetReadingOrderAsync())
{ {
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) continue; if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) continue;
// Some keys are different than FilePath, so we add both to ease loookup
dict.Add(contentFileRef.FilePath, pageCount); // FileName -> FilePath dict.Add(contentFileRef.FilePath, pageCount); // FileName -> FilePath
dict.TryAdd(contentFileRef.Key, pageCount); // FileName -> FilePath
pageCount += 1; pageCount += 1;
} }
@ -861,7 +872,7 @@ public class BookService : IBookService
if (mappings.ContainsKey(CleanContentKeys(key))) return key; if (mappings.ContainsKey(CleanContentKeys(key))) return key;
// Fallback to searching for key (bad epub metadata) // Fallback to searching for key (bad epub metadata)
var correctedKey = book.Content.Html.Local.Keys.FirstOrDefault(s => s.EndsWith(key)); var correctedKey = book.Content.Html.Local.Select(s => s.Key).FirstOrDefault(s => s.EndsWith(key));
if (!string.IsNullOrEmpty(correctedKey)) if (!string.IsNullOrEmpty(correctedKey))
{ {
key = correctedKey; key = correctedKey;
@ -885,17 +896,18 @@ public class BookService : IBookService
public static string CoalesceKeyForAnyFile(EpubBookRef book, string key) public static string CoalesceKeyForAnyFile(EpubBookRef book, string key)
{ {
if (book.Content.AllFiles.Local.ContainsKey(key)) return key; if (book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return key;
var cleanedKey = CleanContentKeys(key); var cleanedKey = CleanContentKeys(key);
if (book.Content.AllFiles.Local.ContainsKey(cleanedKey)) return cleanedKey; if (book.Content.AllFiles.ContainsLocalFileRefWithKey(cleanedKey)) return cleanedKey;
// TODO: Figure this out
// Fallback to searching for key (bad epub metadata) // Fallback to searching for key (bad epub metadata)
var correctedKey = book.Content.AllFiles.Local.Keys.SingleOrDefault(s => s.EndsWith(key)); // var correctedKey = book.Content.AllFiles.Keys.SingleOrDefault(s => s.EndsWith(key));
if (!string.IsNullOrEmpty(correctedKey)) // if (!string.IsNullOrEmpty(correctedKey))
{ // {
key = correctedKey; // key = correctedKey;
} // }
return key; return key;
} }
@ -928,7 +940,7 @@ public class BookService : IBookService
foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null)) foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null))
{ {
var key = CoalesceKey(book, mappings, nestedChapter.Link?.ContentFileName); var key = CoalesceKey(book, mappings, nestedChapter.Link?.ContentFilePath);
if (mappings.TryGetValue(key, out var mapping)) if (mappings.TryGetValue(key, out var mapping))
{ {
nestedChapters.Add(new BookChapterItem nestedChapters.Add(new BookChapterItem
@ -947,12 +959,15 @@ public class BookService : IBookService
if (chaptersList.Count != 0) return chaptersList; if (chaptersList.Count != 0) return chaptersList;
// Generate from TOC from links (any point past this, Kavita is generating as a TOC doesn't exist) // Generate from TOC from links (any point past this, Kavita is generating as a TOC doesn't exist)
var tocPage = book.Content.Html.Local.Keys.FirstOrDefault(k => k.ToUpper().Contains("TOC")); var tocPage = book.Content.Html.Local.Select(s => s.Key).FirstOrDefault(k => k.Equals("TOC.XHTML", StringComparison.InvariantCultureIgnoreCase) ||
if (tocPage == null) return chaptersList; k.Equals("NAVIGATION.XHTML", StringComparison.InvariantCultureIgnoreCase));
if (string.IsNullOrEmpty(tocPage)) return chaptersList;
// Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content // Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content
if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file)) return chaptersList;
var content = await file.ReadContentAsync();
var doc = new HtmlDocument(); var doc = new HtmlDocument();
var content = await book.Content.Html.Local[tocPage].ReadContentAsync();
doc.LoadHtml(content); doc.LoadHtml(content);
var anchors = doc.DocumentNode.SelectNodes("//a"); var anchors = doc.DocumentNode.SelectNodes("//a");
if (anchors == null) return chaptersList; if (anchors == null) return chaptersList;
@ -1096,7 +1111,7 @@ public class BookService : IBookService
} }
else else
{ {
var groupKey = CoalesceKey(book, mappings, navigationItem.Link.ContentFileName); var groupKey = CoalesceKey(book, mappings, navigationItem.Link.ContentFilePath);
if (mappings.ContainsKey(groupKey)) if (mappings.ContainsKey(groupKey))
{ {
chaptersList.Add(new BookChapterItem chaptersList.Add(new BookChapterItem
@ -1133,8 +1148,8 @@ public class BookService : IBookService
{ {
// 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. // 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 var coverImageContent = epubBook.Content.Cover
?? epubBook.Content.Images.Local.Values.FirstOrDefault(file => Parser.IsCoverImage(file.FilePath)) // FileName -> FilePath ?? epubBook.Content.Images.Local.FirstOrDefault(file => Parser.IsCoverImage(file.FilePath)) // FileName -> FilePath
?? epubBook.Content.Images.Local.Values.FirstOrDefault(); ?? epubBook.Content.Images.Local.FirstOrDefault();
if (coverImageContent == null) return string.Empty; if (coverImageContent == null) return string.Empty;
using var stream = coverImageContent.GetContentStream(); using var stream = coverImageContent.GetContentStream();

View File

@ -172,7 +172,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
{ {
using var book = await EpubReader.OpenBookAsync(filePath, BookService.BookReaderOptions); using var book = await EpubReader.OpenBookAsync(filePath, BookService.BookReaderOptions);
var totalPages = book.Content.Html.Local.Values; var totalPages = book.Content.Html.Local;
foreach (var bookPage in totalPages) foreach (var bookPage in totalPages)
{ {
var progress = Math.Max(0F, var progress = Math.Max(0F,

View File

@ -1058,4 +1058,19 @@ public static class Parser
{ {
return string.IsNullOrEmpty(name) ? string.Empty : name.Replace('_', ' '); return string.IsNullOrEmpty(name) ? string.Empty : name.Replace('_', ' ');
} }
public static string? ExtractFilename(string fileUrl)
{
var matches = Parser.CssImageUrlRegex.Matches(fileUrl);
foreach (Match match in matches)
{
if (!match.Success) continue;
// NOTE: This is failing for //localhost:5000/api/book/29919/book-resources?file=OPS/images/tick1.jpg
var importFile = match.Groups["Filename"].Value;
if (!importFile.Contains("?")) return importFile;
}
return null;
}
} }

View File

@ -97,9 +97,8 @@ public class TokenService : ITokenService
} }
var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken); var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken);
if (!validated) if (!validated && tokenContent.ValidTo > DateTime.UtcNow.Add(TimeSpan.FromHours(1)))
{ {
_logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token"); _logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token");
return null; return null;
} }

View File

@ -771,7 +771,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const links = this.readingSectionElemRef.nativeElement.querySelectorAll('a'); const links = this.readingSectionElemRef.nativeElement.querySelectorAll('a');
links.forEach((link: any) => { links.forEach((link: any) => {
link.addEventListener('click', (e: any) => { link.addEventListener('click', (e: any) => {
console.log('Link clicked: ', e);
if (!e.target.attributes.hasOwnProperty('kavita-page')) { return; } if (!e.target.attributes.hasOwnProperty('kavita-page')) { return; }
const page = parseInt(e.target.attributes['kavita-page'].value, 10); const page = parseInt(e.target.attributes['kavita-page'].value, 10);
if (this.adhocPageHistory.peek()?.page !== this.pageNum) { if (this.adhocPageHistory.peek()?.page !== this.pageNum) {

View File

@ -10,7 +10,7 @@
<div class="col-md-8"> <div class="col-md-8">
<a class="col me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" *ngFor="let link of links" [title]="link"> <a class="col me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" *ngFor="let link of links" [title]="link">
<img width="24px" height="24px" #img class="lazyload img-placeholder" <img width="24px" height="24px" #img class="lazyload img-placeholder"
src="data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" [src]="imageService.errorWebLinkImage"
[attr.data-src]="imageService.getWebLinkImage(link)" [attr.data-src]="imageService.getWebLinkImage(link)"
(error)="imageService.updateErroredWebLinkImage($event)" (error)="imageService.updateErroredWebLinkImage($event)"
aria-hidden="true"> aria-hidden="true">
@ -208,9 +208,9 @@
</div> </div>
<div class="row g-0"> <div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperites" > <hr class="col mt-3" *ngIf="hasExtendedProperties" >
<a [class.hidden]="hasExtendedProperites" *ngIf="hasExtendedProperites" <a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties"
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()"> class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i> <i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
See {{isCollapsed ? 'More' : 'Less'}} See {{isCollapsed ? 'More' : 'Less'}}
</a> </a>

View File

@ -29,7 +29,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
@Input() series!: Series; @Input() series!: Series;
isCollapsed: boolean = true; isCollapsed: boolean = true;
hasExtendedProperites: boolean = false; hasExtendedProperties: boolean = false;
imageService = inject(ImageService); imageService = inject(ImageService);
@ -62,7 +62,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this.hasExtendedProperites = this.seriesMetadata.colorists.length > 0 || this.hasExtendedProperties = this.seriesMetadata.colorists.length > 0 ||
this.seriesMetadata.editors.length > 0 || this.seriesMetadata.editors.length > 0 ||
this.seriesMetadata.coverArtists.length > 0 || this.seriesMetadata.coverArtists.length > 0 ||
this.seriesMetadata.inkers.length > 0 || this.seriesMetadata.inkers.length > 0 ||

View File

@ -1,200 +0,0 @@
import { ElementRef, Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DomHelperService {
constructor() {}
// from: https://stackoverflow.com/questions/40597658/equivalent-of-angular-equals-in-angular2#44649659
deepEquals(x: any, y: any) {
if (x === y) {
return true; // if both x and y are null or undefined and exactly the same
} else if (!(x instanceof Object) || !(y instanceof Object)) {
return false; // if they are not strictly equal, they both need to be Objects
} else if (x.constructor !== y.constructor) {
// they must have the exact same prototype chain, the closest we can do is
// test their constructor.
return false;
} else {
for (const p in x) {
if (!x.hasOwnProperty(p)) {
continue; // other properties were tested using x.constructor === y.constructor
}
if (!y.hasOwnProperty(p)) {
return false; // allows to compare x[ p ] and y[ p ] when set to undefined
}
if (x[p] === y[p]) {
continue; // if they have the same strict value or identity then they are equal
}
if (typeof (x[p]) !== 'object') {
return false; // Numbers, Strings, Functions, Booleans must be strictly equal
}
if (!this.deepEquals(x[p], y[p])) {
return false;
}
}
for (const p in y) {
if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) {
return false;
}
}
return true;
}
}
isHidden(node: ElementRef){
const el = node.nativeElement?node.nativeElement:node;
const elemStyle = window.getComputedStyle(el);
return el.style.display === 'none' || elemStyle.visibility === 'hidden' || el.hasAttribute('hidden') || elemStyle.display === 'none';
}
isTabable(node: ElementRef): boolean {
const el = node.nativeElement?node.nativeElement:node;
const tagName = el.tagName;
if(this.isHidden(node)){
return false;
}
// el.attribute:NamdedNodeMap
if (el.attributes.hasOwnProperty('tabindex')) {
return (parseInt(el.attributes.getNamedItem('tabindex'),10) >= 0);
}
if (tagName === 'A' || tagName === 'AREA' || tagName === 'BUTTON' || tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') {
if (tagName === 'A' || tagName === 'AREA') {
return (el.attributes.getNamedItem('href') !== '');
}
return !el.attributes.hasOwnProperty('disabled'); // check for cases when: disabled="true" and disabled="false"
}
return false;
}
private isValidChild(child: any): boolean { // child:ElementRef.nativeElement
return child.nodeType == 1 && child.nodeName != 'SCRIPT' && child.nodeName != 'STYLE';
}
private hasValidParent(obj: any) { // obj:ElementRef.nativeElement
return (this.isValidChild(obj) && obj.parentElement.nodeName !== 'BODY');
}
private traverse(obj: any, fromTop: boolean): ElementRef | undefined | boolean {
// obj:ElementRef||ElementRef.nativeElement
var obj = obj? (obj.nativeElement?obj.nativeElement:obj) : document.getElementsByTagName('body')[0];
if (this.isValidChild(obj) && this.isTabable(obj)) {
return obj;
}
// If object is hidden, skip it's children
if (this.isValidChild(obj) && this.isHidden(obj)) {
return undefined;
}
// If object is hidden, skip it's children
if (obj.classList && obj.classList.contains('ng-hide')) { // some nodes don't have classList?!
return false;
}
if (obj.hasChildNodes()) {
var child;
if (fromTop) {
child = obj.firstChild;
} else {
child = obj.lastChild;
}
while(child) {
var res = this.traverse(child, fromTop);
if(res){
return res;
}
else{
if (fromTop) {
child = child.nextSibling;
} else {
child = child.previousSibling;
}
}
}
}
else{
return undefined;
}
}
previousElement(el: any, isFocusable: boolean): any { // ElementRef | undefined | boolean
var elem = el.nativeElement ? el.nativeElement : el;
if (el.hasOwnProperty('length')) {
elem = el[0];
}
var parent = elem.parentElement;
var previousElem = undefined;
if(isFocusable) {
if (this.hasValidParent(elem)) {
var siblings = parent.children;
if (siblings.length > 0) {
// Good practice to splice out the elem from siblings if there, saving some time.
// We allow for a quick check for jumping to parent first before removing.
if (siblings[0] === elem) {
// If we are looking at immidiate parent and elem is first child, we need to go higher
var e = this.previousElement(elem.parentNode, isFocusable);
if (this.isTabable(e)) {
return e;
}
} else {
// I need to filter myself and any nodes next to me from the siblings
var indexOfElem = Array.prototype.indexOf.call(siblings, elem);
const that = this;
siblings = Array.prototype.filter.call(siblings, function(item, itemIndex) {
if (!that.deepEquals(elem, item) && itemIndex < indexOfElem) {
return true;
}
});
}
// We need to search backwards
for (var i = 0; i <= siblings.length-1; i++) {//for (var i = siblings.length-1; i >= 0; i--) {
var ret = this.traverse(siblings[i], false);
if (ret !== undefined) {
return ret;
}
}
var e = this.previousElement(elem.parentNode, isFocusable);
if (this.isTabable(e)) {
return e;
}
}
}
} else {
var siblings = parent.children;
if (siblings.length > 1) {
// Since indexOf is on Array.prototype and parent.children is a NodeList, we have to use call()
var index = Array.prototype.indexOf.call(siblings, elem);
previousElem = siblings[index-1];
}
}
return previousElem;
};
lastTabableElement(el: any) {
/* This will return the first tabable element from the parent el */
var elem = el.nativeElement?el.nativeElement:el;
if (el.hasOwnProperty('length')) {
elem = el[0];
}
return this.traverse(elem, false);
};
firstTabableElement(el: any) {
/* This will return the first tabable element from the parent el */
var elem = el.nativeElement ? el.nativeElement : el;
if (el.hasOwnProperty('length')) {
elem = el[0];
}
return this.traverse(elem, true);
};
isInDOM(obj: Node) {
return document.documentElement.contains(obj);
}
}

View File

@ -214,6 +214,7 @@ export class DownloadService {
).pipe( ).pipe(
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
download((blob, filename) => { download((blob, filename) => {
console.log('saving: ', filename)
this.save(blob, decodeURIComponent(filename)); this.save(blob, decodeURIComponent(filename));
}), }),
tap((d) => this.updateDownloadState(d, downloadType, subtitle)), tap((d) => this.updateDownloadState(d, downloadType, subtitle)),

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.2.11" "version": "0.7.2.12"
}, },
"servers": [ "servers": [
{ {