mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
ISBN Support (#1985)
* Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes
This commit is contained in:
parent
a293500f42
commit
6be9ee39f4
@ -79,6 +79,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||
<PackageReference Include="NetVips" Version="2.3.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.14.2" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.6" />
|
||||
|
@ -173,6 +173,7 @@ public class ImageController : BaseApiController
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
if (string.IsNullOrEmpty(url)) return BadRequest("Url cannot be null");
|
||||
|
||||
// Check if the domain exists
|
||||
var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url));
|
||||
|
@ -97,4 +97,9 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate
|
||||
/// Comma-separated link of urls to external services that have some relation to the Chapter
|
||||
/// </summary>
|
||||
public string WebLinks { get; set; }
|
||||
/// <summary>
|
||||
/// ISBN-13 (usually) of the Chapter
|
||||
/// </summary>
|
||||
/// <remarks>This is guaranteed to be Valid</remarks>
|
||||
public string ISBN { get; set; }
|
||||
}
|
||||
|
@ -121,6 +121,10 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
builder.Entity<SeriesMetadata>()
|
||||
.Property(b => b.WebLinks)
|
||||
.HasDefaultValue(string.Empty);
|
||||
|
||||
builder.Entity<Chapter>()
|
||||
.Property(b => b.ISBN)
|
||||
.HasDefaultValue(string.Empty);
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,7 +2,9 @@
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using Kavita.Common.Extensions;
|
||||
using Nager.ArticleNumber;
|
||||
|
||||
namespace API.Data.Metadata;
|
||||
|
||||
@ -35,6 +37,17 @@ public class ComicInfo
|
||||
/// IETF BCP 47 Code to represent the language of the content
|
||||
/// </summary>
|
||||
public string LanguageISO { get; set; } = string.Empty;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
/// <summary>
|
||||
/// ISBN for the underlying document
|
||||
/// </summary>
|
||||
/// <remarks>ComicInfo.xml will actually output a GTIN (Global Trade Item Number) and it is the responsibility of the Parser to extract the ISBN. EPub will return ISBN.</remarks>
|
||||
public string Isbn { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This is only for deserialization and used within <see cref="ArchiveService"/>. Use <see cref="Isbn"/> for the actual value.
|
||||
/// </summary>
|
||||
public string GTIN { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This is the link to where the data was scraped from
|
||||
/// </summary>
|
||||
@ -138,6 +151,22 @@ public class ComicInfo
|
||||
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
|
||||
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
|
||||
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
|
||||
|
||||
// We need to convert GTIN to ISBN
|
||||
if (!string.IsNullOrEmpty(info.GTIN) && ArticleNumberHelper.IsValidGtin(info.GTIN))
|
||||
{
|
||||
// This is likely a valid ISBN
|
||||
if (info.GTIN[0] == '0')
|
||||
{
|
||||
var potentialISBN = info.GTIN.Substring(1, info.GTIN.Length - 1);
|
||||
if (ArticleNumberHelper.IsValidIsbn13(potentialISBN))
|
||||
{
|
||||
info.Isbn = potentialISBN;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
1927
API/Data/Migrations/20230512004545_ChapterISBN.Designer.cs
generated
Normal file
1927
API/Data/Migrations/20230512004545_ChapterISBN.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
API/Data/Migrations/20230512004545_ChapterISBN.cs
Normal file
29
API/Data/Migrations/20230512004545_ChapterISBN.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChapterISBN : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ISBN",
|
||||
table: "Chapter",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
defaultValue: string.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ISBN",
|
||||
table: "Chapter");
|
||||
}
|
||||
}
|
||||
}
|
@ -413,6 +413,11 @@ namespace API.Data.Migrations
|
||||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ISBN")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<bool>("IsSpecial")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -104,6 +104,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
|
||||
/// Comma-separated link of urls to external services that have some relation to the Chapter
|
||||
/// </summary>
|
||||
public string WebLinks { get; set; } = string.Empty;
|
||||
public string ISBN { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// All people attached at a Chapter level. Usually Comics will have different people per issue.
|
||||
|
@ -11,6 +11,7 @@ using API.Data.Metadata;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Docnet.Core;
|
||||
using Docnet.Core.Converters;
|
||||
@ -21,6 +22,7 @@ using HtmlAgilityPack;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IO;
|
||||
using Nager.ArticleNumber;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using VersOne.Epub;
|
||||
@ -433,6 +435,14 @@ public class BookService : IBookService
|
||||
};
|
||||
ComicInfo.CleanComicInfo(info);
|
||||
|
||||
foreach (var identifier in epubBook.Schema.Package.Metadata.Identifiers.Where(id => id.Scheme.Equals("ISBN")))
|
||||
{
|
||||
var isbn = identifier.Identifier.Replace("urn:isbn:", string.Empty);
|
||||
if (!ArticleNumberHelper.IsValidIsbn10(isbn) && !ArticleNumberHelper.IsValidIsbn13(isbn)) continue;
|
||||
info.Isbn = isbn;
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse tags not exposed via Library
|
||||
foreach (var metadataItem in epubBook.Schema.Package.Metadata.MetaItems)
|
||||
{
|
||||
|
@ -1,18 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Flurl;
|
||||
using Flurl.Http;
|
||||
using HtmlAgilityPack;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Image = NetVips.Image;
|
||||
using Size = SixLabors.ImageSharp.Size;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
@ -194,17 +191,46 @@ public class ImageService : IImageService
|
||||
var baseUrl = uri.Scheme + "://" + uri.Host;
|
||||
try
|
||||
{
|
||||
var validIconRelations = new[]
|
||||
{
|
||||
"icon",
|
||||
"apple-touch-icon",
|
||||
};
|
||||
var htmlContent = url.GetStringAsync().Result;
|
||||
var htmlDocument = new HtmlDocument();
|
||||
htmlDocument.LoadHtml(htmlContent);
|
||||
var pngLinks = htmlDocument.DocumentNode.Descendants("link")
|
||||
.Where(link => validIconRelations.Contains(link.GetAttributeValue("rel", string.Empty)))
|
||||
.Select(link => link.GetAttributeValue("href", string.Empty))
|
||||
.Where(href => href.EndsWith(".png") || href.EndsWith(".PNG"))
|
||||
.ToList();
|
||||
|
||||
if (pngLinks == null)
|
||||
{
|
||||
throw new KavitaException($"Could not grab favicon from {baseUrl}");
|
||||
}
|
||||
var correctSizeLink = pngLinks.FirstOrDefault(pngLink => pngLink.Contains("32")) ?? pngLinks.FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(correctSizeLink))
|
||||
{
|
||||
throw new KavitaException($"Could not grab favicon from {baseUrl}");
|
||||
}
|
||||
|
||||
var finalUrl = correctSizeLink;
|
||||
if (!correctSizeLink.StartsWith(uri.Scheme))
|
||||
{
|
||||
finalUrl = Url.Combine(baseUrl, correctSizeLink);
|
||||
}
|
||||
|
||||
_logger.LogTrace("Fetching favicon from {Url}", finalUrl);
|
||||
// Download the favicon.ico file using Flurl
|
||||
var faviconStream = await baseUrl
|
||||
.AppendPathSegment("favicon.ico")
|
||||
.AllowHttpStatus("2xx")
|
||||
var faviconStream = await finalUrl
|
||||
.AllowHttpStatus("2xx,304")
|
||||
.GetStreamAsync();
|
||||
|
||||
// Create the destination file path
|
||||
var filename = $"{domain}.png";
|
||||
using var icon = new Icon(faviconStream);
|
||||
using var bitmap = icon.ToBitmap();
|
||||
bitmap.Save(Path.Combine(_directoryService.FaviconDirectory, filename), ImageFormat.Png);
|
||||
using var image = Image.PngloadStream(faviconStream);
|
||||
image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
|
||||
_logger.LogDebug("Favicon.png for {Domain} downloaded and saved successfully", domain);
|
||||
return filename;
|
||||
|
@ -36,7 +36,7 @@ public interface IProcessSeries
|
||||
void UpdateVolumes(Series series, IList<ParserInfo> parsedInfos, bool forceUpdate = false);
|
||||
void UpdateChapters(Series series, Volume volume, IList<ParserInfo> parsedInfos, bool forceUpdate = false);
|
||||
void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info, bool forceUpdate = false);
|
||||
void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? info);
|
||||
void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? comicInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -722,6 +722,11 @@ public class ProcessSeries : IProcessSeries
|
||||
);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(comicInfo.Isbn))
|
||||
{
|
||||
chapter.ISBN = comicInfo.Isbn;
|
||||
}
|
||||
|
||||
if (comicInfo.Count > 0)
|
||||
{
|
||||
chapter.TotalCount = comicInfo.Count;
|
||||
|
@ -42,4 +42,5 @@ export interface Chapter {
|
||||
*/
|
||||
volumeTitle?: string;
|
||||
webLinks: string;
|
||||
isbn: string;
|
||||
}
|
||||
|
@ -85,5 +85,14 @@
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.isbn.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ISBN" [clickable]="false" fontClasses="fa-solid fa-barcode" title="ISBN">
|
||||
{{chapter.isbn}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
@ -56,6 +56,7 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get WebLinks() {
|
||||
if (this.chapter.webLinks === '') return [];
|
||||
return this.chapter.webLinks.split(',');
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ export class BytesPipe implements PipeTransform {
|
||||
*
|
||||
* Credit: https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
|
||||
*/
|
||||
transform(bytes: number, si=true, dp=0): string {
|
||||
transform(bytes: number, si=true, dp=1): string {
|
||||
const thresh = si ? 1000 : 1024;
|
||||
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
@ -35,8 +35,12 @@ export class BytesPipe implements PipeTransform {
|
||||
++u;
|
||||
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||
|
||||
const fixed = bytes.toFixed(dp);
|
||||
if ((fixed + '').endsWith('.0')) {
|
||||
return bytes.toFixed(0) + ' ' + units[u];
|
||||
}
|
||||
|
||||
return bytes.toFixed(dp) + ' ' + units[u];
|
||||
return fixed + ' ' + units[u];
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
||||
}
|
||||
|
||||
get WebLinks() {
|
||||
if (this.seriesMetadata?.webLinks === '') return [];
|
||||
return this.seriesMetadata?.webLinks.split(',') || [];
|
||||
}
|
||||
|
||||
|
@ -10591,6 +10591,10 @@
|
||||
"description": "Comma-separated link of urls to external services that have some relation to the Chapter",
|
||||
"nullable": true
|
||||
},
|
||||
"isbn": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"people": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -10750,6 +10754,11 @@
|
||||
"type": "string",
|
||||
"description": "Comma-separated link of urls to external services that have some relation to the Chapter",
|
||||
"nullable": true
|
||||
},
|
||||
"isbn": {
|
||||
"type": "string",
|
||||
"description": "ISBN-13 (usually) of the Chapter",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
Loading…
x
Reference in New Issue
Block a user