More Polish (#2005)

* Implemented sort title extraction from epub 3 files.

* Added link to wiki for media errors

* Fixed the hack to reduce JWT refresh token expiration

* Fixed up a case where favicon downloading wasn't correcting links that started with // correctly.

Added a fallback for sites that just don't pngs available.

* Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community.

* Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler.
This commit is contained in:
Joe Milazzo 2023-05-20 09:21:27 -05:00 committed by GitHub
parent e34fcc10cf
commit 3c887f5377
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 112 additions and 33 deletions

View File

@ -144,4 +144,14 @@ public static class Seed
} }
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
// /// <summary>
// /// Responsible to copy (not overwrite) a set of favicons that Kavita can't parse from websites.
// /// </summary>
// /// <param name="directoryService"></param>
// /// <returns></returns>
// public static Task SeedFavicons(IDirectoryService directoryService)
// {
//
// }
} }

View File

@ -472,7 +472,10 @@ public class BookService : IBookService
break; break;
case "calibre:series": case "calibre:series":
info.Series = metadataItem.Content; info.Series = metadataItem.Content;
info.SeriesSort = metadataItem.Content; if (string.IsNullOrEmpty(info.SeriesSort))
{
info.SeriesSort = metadataItem.Content;
}
break; break;
case "calibre:series_index": case "calibre:series_index":
info.Volume = metadataItem.Content; info.Volume = metadataItem.Content;
@ -488,7 +491,10 @@ public class BookService : IBookService
break; break;
case "belongs-to-collection": case "belongs-to-collection":
info.Series = metadataItem.Content; info.Series = metadataItem.Content;
info.SeriesSort = metadataItem.Content; if (string.IsNullOrEmpty(info.SeriesSort))
{
info.SeriesSort = metadataItem.Content;
}
break; break;
case "collection-type": case "collection-type":
// These look to be genres from https://manual.calibre-ebook.com/sub_groups.html or can be "series" // These look to be genres from https://manual.calibre-ebook.com/sub_groups.html or can be "series"
@ -504,32 +510,24 @@ public class BookService : IBookService
PopulatePerson(metadataItem, info, person); PopulatePerson(metadataItem, info, person);
break; break;
case "title-type": case "title-type":
if (!metadataItem.Content.Equals("collection")) break; if (metadataItem.Content.Equals("collection"))
var titleId = metadataItem.Refines?.Replace("#", string.Empty); {
var readingListElem = epubBook.Schema.Package.Metadata.Titles ExtractCollectionOrReadingList(metadataItem, epubBook, info);
.FirstOrDefault(item => item.Id == titleId); }
if (readingListElem == null) break;
var count = epubBook.Schema.Package.Metadata.MetaItems if (metadataItem.Content.Equals("main"))
.FirstOrDefault(item =>
item.Property == "display-seq" && item.Refines == metadataItem.Refines);
if (count == null || count.Content == "0")
{ {
// TODO: Rewrite this to use a StringBuilder ExtractSortTitle(metadataItem, epubBook, info);
// Treat this as a Collection
info.SeriesGroup += (string.IsNullOrEmpty(info.StoryArc) ? string.Empty : ",") + readingListElem.Title.Replace(",", "_");
}
else
{
// Treat as a reading list
info.AlternateSeries += (string.IsNullOrEmpty(info.AlternateSeries) ? string.Empty : ",") + readingListElem.Title.Replace(",", "_");
info.AlternateNumber += (string.IsNullOrEmpty(info.AlternateNumber) ? string.Empty : ",") + count.Content;
} }
break; break;
} }
} }
// Check if there is a SortTitle
// Include regular Writer as well, for cases where there is no special tag // Include regular Writer as well, for cases where there is no special tag
info.Writer = string.Join(",", info.Writer = string.Join(",",
epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.CleanAuthor(c.Creator))); epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.CleanAuthor(c.Creator)));
@ -556,6 +554,46 @@ public class BookService : IBookService
return null; return null;
} }
private static void ExtractSortTitle(EpubMetadataMeta metadataItem, EpubBookRef epubBook, ComicInfo info)
{
var titleId = metadataItem.Refines?.Replace("#", string.Empty);
var titleElem = epubBook.Schema.Package.Metadata.Titles
.FirstOrDefault(item => item.Id == titleId);
if (titleElem == null) return;
var sortTitleElem = epubBook.Schema.Package.Metadata.MetaItems
.FirstOrDefault(item =>
item.Property == "file-as" && item.Refines == metadataItem.Refines);
if (sortTitleElem == null || string.IsNullOrWhiteSpace(sortTitleElem.Content)) return;
info.SeriesSort = sortTitleElem.Content;
}
private static void ExtractCollectionOrReadingList(EpubMetadataMeta metadataItem, EpubBookRef epubBook, ComicInfo info)
{
var titleId = metadataItem.Refines?.Replace("#", string.Empty);
var readingListElem = epubBook.Schema.Package.Metadata.Titles
.FirstOrDefault(item => item.Id == titleId);
if (readingListElem == null) return;
var count = epubBook.Schema.Package.Metadata.MetaItems
.FirstOrDefault(item =>
item.Property == "display-seq" && item.Refines == metadataItem.Refines);
if (count == null || count.Content == "0")
{
// TODO: Rewrite this to use a StringBuilder
// Treat this as a Collection
info.SeriesGroup += (string.IsNullOrEmpty(info.StoryArc) ? string.Empty : ",") +
readingListElem.Title.Replace(",", "_");
}
else
{
// Treat as a reading list
info.AlternateSeries += (string.IsNullOrEmpty(info.AlternateSeries) ? string.Empty : ",") +
readingListElem.Title.Replace(",", "_");
info.AlternateNumber += (string.IsNullOrEmpty(info.AlternateNumber) ? string.Empty : ",") + count.Content;
}
}
private static void PopulatePerson(EpubMetadataMeta metadataItem, ComicInfo info, EpubMetadataCreator person) private static void PopulatePerson(EpubMetadataMeta metadataItem, ComicInfo info, EpubMetadataCreator person)
{ {
switch (metadataItem.Content) switch (metadataItem.Content)

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -227,6 +226,8 @@ public class ImageService : IImageService
url = value; url = value;
} }
var correctSizeLink = string.Empty;
try try
{ {
var htmlContent = url.GetStringAsync().Result; var htmlContent = url.GetStringAsync().Result;
@ -238,11 +239,16 @@ public class ImageService : IImageService
.Where(href => href.Split("?")[0].EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) .Where(href => href.Split("?")[0].EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
.ToList(); .ToList();
if (pngLinks == null) correctSizeLink = (pngLinks?.FirstOrDefault(pngLink => pngLink.Contains("32")) ?? pngLinks?.FirstOrDefault());
{ }
throw new KavitaException($"Could not grab favicon from {baseUrl}"); catch (Exception ex)
} {
var correctSizeLink = pngLinks.FirstOrDefault(pngLink => pngLink.Contains("32")) ?? pngLinks.FirstOrDefault(); _logger.LogError(ex, "Error downloading favicon.png for {Domain}, will try fallback methods", domain);
}
try
{
correctSizeLink = FallbackToKavitaReaderFavicon(baseUrl);
if (string.IsNullOrEmpty(correctSizeLink)) if (string.IsNullOrEmpty(correctSizeLink))
{ {
throw new KavitaException($"Could not grab favicon from {baseUrl}"); throw new KavitaException($"Could not grab favicon from {baseUrl}");
@ -253,7 +259,7 @@ public class ImageService : IImageService
// If starts with //, it's coming usually from an offsite cdn // If starts with //, it's coming usually from an offsite cdn
if (correctSizeLink.StartsWith("//")) if (correctSizeLink.StartsWith("//"))
{ {
finalUrl = Url.Combine("https:", correctSizeLink); finalUrl = "https:" + correctSizeLink;
} }
else if (!correctSizeLink.StartsWith(uri.Scheme)) else if (!correctSizeLink.StartsWith(uri.Scheme))
{ {
@ -287,14 +293,37 @@ public class ImageService : IImageService
_logger.LogDebug("Favicon.png for {Domain} downloaded and saved successfully", domain); _logger.LogDebug("Favicon.png for {Domain} downloaded and saved successfully", domain);
return filename; return filename;
} }catch (Exception ex)
catch (Exception ex)
{ {
_logger.LogError(ex, "Error downloading favicon.png for {Domain}", domain); _logger.LogError(ex, "Error downloading favicon.png for {Domain}", domain);
throw; throw;
} }
} }
private static string FallbackToKavitaReaderFavicon(string baseUrl)
{
var correctSizeLink = string.Empty;
var allOverrides = "https://kavitareader.com/assets/favicons/urls.txt".GetStringAsync().Result;
if (!string.IsNullOrEmpty(allOverrides))
{
var cleanedBaseUrl = baseUrl.Replace("https://", string.Empty);
var externalFile = allOverrides
.Split("\n")
.FirstOrDefault(url =>
cleanedBaseUrl.Equals(url.Replace(".png", string.Empty)) ||
cleanedBaseUrl.Replace("www.", string.Empty).Equals(url.Replace(".png", string.Empty)
));
if (string.IsNullOrEmpty(externalFile))
{
throw new KavitaException($"Could not grab favicon from {baseUrl}");
}
correctSizeLink = "https://kavitareader.com/assets/favicons/" + externalFile;
}
return correctSizeLink;
}
/// <inheritdoc /> /// <inheritdoc />
public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth) public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth)
{ {

View File

@ -97,7 +97,7 @@ 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 && tokenContent.ValidTo > DateTime.UtcNow.Add(TimeSpan.FromHours(1))) 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

@ -1,4 +1,6 @@
<p>This table contains issues found during scan or reading of your media. This list is non-managed. You can clear it at any time and use Library (Force) Scan to perform analysis.</p> <p>This table contains issues found during scan or reading of your media. This list is non-managed.
You can clear it at any time and use Library (Force) Scan to perform analysis. A list of some common errors and what
they mean can be found on the <a rel="noopener noreferrer" target="_blank" href="https://wiki.kavitareader.com/en/guides/managing-your-files/scanner#media-errors">wiki</a>.</p>
<form [formGroup]="formGroup"> <form [formGroup]="formGroup">
<div class="row g-0 mb-3"> <div class="row g-0 mb-3">

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.12" "version": "0.7.2.13"
}, },
"servers": [ "servers": [
{ {