mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 13:44:31 -04:00
Feature/misc (#1234)
* Fixed a bug where publication status could show as filled in when total number is 0 but there is a max count. Add ComicInfo support for LocalizedSeries which will populate a Series LocalizedName. Fixed an issue in tag constraint issues. * Hooked in LocalizedSeries tag into merge step in scanner. * Hooked in LocalizedSeries from ComicInfo into Kavita and also use it to help during merge phase to avoid 2 different series, if one file is using the name of the localized series. * Reduced some extra string creation and updated epub library to ignore bad ToCs. * Bumped dependencies to latest. When an epub doesn't have a dc:date with publication event type, default back to just a normal dc:date tag. * Fixed a bug where webtoon reader would error out on first load due to how we passed the function to the reader * Reverted the centering code
This commit is contained in:
parent
0eb3d74ff9
commit
1e51e39f66
@ -7,10 +7,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="4.3.0" />
|
<PackageReference Include="NSubstitute" Version="4.3.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="16.1.25" />
|
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="17.0.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -16,7 +16,7 @@ public class ComicInfoTests
|
|||||||
[InlineData("Early Childhood", AgeRating.EarlyChildhood)]
|
[InlineData("Early Childhood", AgeRating.EarlyChildhood)]
|
||||||
[InlineData("Everyone 10+", AgeRating.Everyone10Plus)]
|
[InlineData("Everyone 10+", AgeRating.Everyone10Plus)]
|
||||||
[InlineData("M", AgeRating.Mature)]
|
[InlineData("M", AgeRating.Mature)]
|
||||||
[InlineData("MA 15+", AgeRating.Mature15Plus)]
|
[InlineData("MA15+", AgeRating.Mature15Plus)]
|
||||||
[InlineData("Mature 17+", AgeRating.Mature17Plus)]
|
[InlineData("Mature 17+", AgeRating.Mature17Plus)]
|
||||||
[InlineData("Rating Pending", AgeRating.RatingPending)]
|
[InlineData("Rating Pending", AgeRating.RatingPending)]
|
||||||
[InlineData("X18+", AgeRating.X18Plus)]
|
[InlineData("X18+", AgeRating.X18Plus)]
|
||||||
|
@ -39,40 +39,40 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||||
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.2" />
|
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.2" />
|
||||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
<PackageReference Include="ExCSS" Version="4.1.3" />
|
||||||
<PackageReference Include="Flurl" Version="3.0.4" />
|
<PackageReference Include="Flurl" Version="3.0.5" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.2" />
|
<PackageReference Include="Flurl.Http" Version="3.2.3" />
|
||||||
<PackageReference Include="Hangfire" Version="1.7.28" />
|
<PackageReference Include="Hangfire" Version="1.7.28" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.28" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.28" />
|
||||||
<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" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
|
||||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
||||||
<PackageReference Include="NetVips" Version="2.1.0" />
|
<PackageReference Include="NetVips" Version="2.1.0" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.12.2" />
|
<PackageReference Include="NetVips.Native" Version="8.12.2" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.1.4" />
|
<PackageReference Include="NReco.Logging.File" Version="1.1.4" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.0" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.1" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.37.0.45539">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.38.0.46746">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.17.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.17.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="16.1.25" />
|
<PackageReference Include="System.IO.Abstractions" Version="17.0.3" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
<PackageReference Include="VersOne.Epub" Version="3.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -40,7 +40,7 @@ namespace API.Controllers
|
|||||||
if (dto.SeriesFormat == MangaFormat.Epub)
|
if (dto.SeriesFormat == MangaFormat.Epub)
|
||||||
{
|
{
|
||||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath);
|
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
|
||||||
bookTitle = book.Title;
|
bookTitle = book.Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||||
{
|
{
|
||||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||||
var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||||
|
|
||||||
var key = BookService.CleanContentKeys(file);
|
var key = BookService.CleanContentKeys(file);
|
||||||
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
|
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
|
||||||
@ -87,7 +87,7 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||||
{
|
{
|
||||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||||
|
|
||||||
var navItems = await book.GetNavigationAsync();
|
var navItems = await book.GetNavigationAsync();
|
||||||
@ -212,7 +212,7 @@ namespace API.Controllers
|
|||||||
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
||||||
|
|
||||||
|
|
||||||
using var book = await EpubReader.OpenBookAsync(path);
|
using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
|
||||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||||
|
|
||||||
var counter = 0;
|
var counter = 0;
|
||||||
|
@ -14,6 +14,10 @@ namespace API.Data.Metadata
|
|||||||
public string Summary { get; set; } = string.Empty;
|
public string Summary { get; set; } = string.Empty;
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
public string Series { get; set; } = string.Empty;
|
public string Series { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Localized Series name. Not standard.
|
||||||
|
/// </summary>
|
||||||
|
public string LocalizedSeries { get; set; } = string.Empty;
|
||||||
public string SeriesSort { get; set; } = string.Empty;
|
public string SeriesSort { get; set; } = string.Empty;
|
||||||
public string Number { get; set; } = string.Empty;
|
public string Number { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -94,6 +98,10 @@ namespace API.Data.Metadata
|
|||||||
{
|
{
|
||||||
if (info == null) return;
|
if (info == null) return;
|
||||||
|
|
||||||
|
info.Series = info.Series.Trim();
|
||||||
|
info.SeriesSort = info.SeriesSort.Trim();
|
||||||
|
info.LocalizedSeries = info.LocalizedSeries.Trim();
|
||||||
|
|
||||||
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
|
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
|
||||||
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
|
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
|
||||||
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
|
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
|
||||||
|
@ -27,7 +27,7 @@ public enum AgeRating
|
|||||||
KidsToAdults = 7,
|
KidsToAdults = 7,
|
||||||
[Description("Teen")]
|
[Description("Teen")]
|
||||||
Teen = 8,
|
Teen = 8,
|
||||||
[Description("MA 15+")]
|
[Description("MA15+")]
|
||||||
Mature15Plus = 9,
|
Mature15Plus = 9,
|
||||||
[Description("Mature 17+")]
|
[Description("Mature 17+")]
|
||||||
Mature17Plus = 10,
|
Mature17Plus = 10,
|
||||||
|
@ -22,6 +22,10 @@ namespace API.Parser
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string SeriesSort { get; set; } = string.Empty;
|
public string SeriesSort { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the LocalizedName field on <see cref="Entities.Series"/>
|
||||||
|
/// </summary>
|
||||||
|
public string LocalizedSeries { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
/// Represents the parsed volumes from a file. By default, will be 0 which means that nothing could be parsed.
|
/// Represents the parsed volumes from a file. By default, will be 0 which means that nothing could be parsed.
|
||||||
/// If Volumes is 0 and Chapters is 0, the file is a special. If Chapters is non-zero, then no volume could be parsed.
|
/// If Volumes is 0 and Chapters is 0, the file is a special. If Chapters is non-zero, then no volume could be parsed.
|
||||||
/// <example>Beastars Vol 3-4 will map to "3-4"</example>
|
/// <example>Beastars Vol 3-4 will map to "3-4"</example>
|
||||||
|
@ -20,6 +20,7 @@ using Microsoft.IO;
|
|||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using VersOne.Epub;
|
using VersOne.Epub;
|
||||||
|
using VersOne.Epub.Options;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
|
||||||
namespace API.Services
|
namespace API.Services
|
||||||
@ -59,6 +60,13 @@ namespace API.Services
|
|||||||
private readonly StylesheetParser _cssParser = new ();
|
private readonly StylesheetParser _cssParser = new ();
|
||||||
private static readonly RecyclableMemoryStreamManager StreamManager = new ();
|
private static readonly RecyclableMemoryStreamManager StreamManager = new ();
|
||||||
private const string CssScopeClass = ".book-content";
|
private const string CssScopeClass = ".book-content";
|
||||||
|
public static readonly EpubReaderOptions BookReaderOptions = new()
|
||||||
|
{
|
||||||
|
PackageReaderOptions = new PackageReaderOptions()
|
||||||
|
{
|
||||||
|
IgnoreMissingToc = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public BookService(ILogger<BookService> logger, IDirectoryService directoryService, IImageService imageService)
|
public BookService(ILogger<BookService> logger, IDirectoryService directoryService, IImageService imageService)
|
||||||
{
|
{
|
||||||
@ -383,10 +391,14 @@ namespace API.Services
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var epubBook = EpubReader.OpenBook(filePath);
|
using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions);
|
||||||
var publicationDate =
|
var publicationDate =
|
||||||
epubBook.Schema.Package.Metadata.Dates.FirstOrDefault(date => date.Event == "publication")?.Date;
|
epubBook.Schema.Package.Metadata.Dates.FirstOrDefault(date => date.Event == "publication")?.Date;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(publicationDate))
|
||||||
|
{
|
||||||
|
publicationDate = epubBook.Schema.Package.Metadata.Dates.FirstOrDefault()?.Date;
|
||||||
|
}
|
||||||
var info = new ComicInfo()
|
var info = new ComicInfo()
|
||||||
{
|
{
|
||||||
Summary = epubBook.Schema.Package.Metadata.Description,
|
Summary = epubBook.Schema.Package.Metadata.Description,
|
||||||
@ -450,7 +462,7 @@ namespace API.Services
|
|||||||
return docReader.GetPageCount();
|
return docReader.GetPageCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
using var epubBook = EpubReader.OpenBook(filePath);
|
using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions);
|
||||||
return epubBook.Content.Html.Count;
|
return epubBook.Content.Html.Count;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -504,7 +516,7 @@ namespace API.Services
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var epubBook = EpubReader.OpenBook(filePath);
|
using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions);
|
||||||
|
|
||||||
// <meta content="The Dark Tower" name="calibre:series"/>
|
// <meta content="The Dark Tower" name="calibre:series"/>
|
||||||
// <meta content="Wolves of the Calla" name="calibre:title_sort"/>
|
// <meta content="Wolves of the Calla" name="calibre:title_sort"/>
|
||||||
@ -669,8 +681,7 @@ namespace API.Services
|
|||||||
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory);
|
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var epubBook = EpubReader.OpenBook(fileFilePath);
|
using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions);
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -254,7 +254,6 @@ public class SeriesService : ISeriesService
|
|||||||
// At this point, all tags that aren't in dto have been removed.
|
// At this point, all tags that aren't in dto have been removed.
|
||||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||||
{
|
{
|
||||||
// This should be normalized name
|
|
||||||
var normalizedTitle = Parser.Parser.Normalize(tagTitle);
|
var normalizedTitle = Parser.Parser.Normalize(tagTitle);
|
||||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle == normalizedTitle);
|
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle == normalizedTitle);
|
||||||
if (existingTag != null)
|
if (existingTag != null)
|
||||||
@ -299,10 +298,11 @@ public class SeriesService : ISeriesService
|
|||||||
// At this point, all tags that aren't in dto have been removed.
|
// At this point, all tags that aren't in dto have been removed.
|
||||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||||
{
|
{
|
||||||
var existingTag = allTags.SingleOrDefault(t => t.Title == tagTitle);
|
var normalizedTitle = Parser.Parser.Normalize(tagTitle);
|
||||||
|
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle));
|
||||||
if (existingTag != null)
|
if (existingTag != null)
|
||||||
{
|
{
|
||||||
if (series.Metadata.Tags.All(t => t.Title != tagTitle))
|
if (series.Metadata.Tags.All(t => t.NormalizedTitle != normalizedTitle))
|
||||||
{
|
{
|
||||||
|
|
||||||
handleAdd(existingTag);
|
handleAdd(existingTag);
|
||||||
|
@ -126,6 +126,11 @@ namespace API.Services.Tasks.Scanner
|
|||||||
{
|
{
|
||||||
info.SeriesSort = info.ComicInfo.SeriesSort.Trim();
|
info.SeriesSort = info.ComicInfo.SeriesSort.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(info.ComicInfo.LocalizedSeries))
|
||||||
|
{
|
||||||
|
info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSeries(info);
|
TrackSeries(info);
|
||||||
@ -144,13 +149,16 @@ namespace API.Services.Tasks.Scanner
|
|||||||
// Check if normalized info.Series already exists and if so, update info to use that name instead
|
// Check if normalized info.Series already exists and if so, update info to use that name instead
|
||||||
info.Series = MergeName(info);
|
info.Series = MergeName(info);
|
||||||
|
|
||||||
|
var normalizedSeries = Parser.Parser.Normalize(info.Series);
|
||||||
|
var normalizedLocalizedSeries = Parser.Parser.Normalize(info.LocalizedSeries);
|
||||||
var existingKey = _scannedSeries.Keys.FirstOrDefault(ps =>
|
var existingKey = _scannedSeries.Keys.FirstOrDefault(ps =>
|
||||||
ps.Format == info.Format && ps.NormalizedName == Parser.Parser.Normalize(info.Series));
|
ps.Format == info.Format && (ps.NormalizedName == normalizedSeries
|
||||||
|
|| ps.NormalizedName == normalizedLocalizedSeries));
|
||||||
existingKey ??= new ParsedSeries()
|
existingKey ??= new ParsedSeries()
|
||||||
{
|
{
|
||||||
Format = info.Format,
|
Format = info.Format,
|
||||||
Name = info.Series,
|
Name = info.Series,
|
||||||
NormalizedName = Parser.Parser.Normalize(info.Series)
|
NormalizedName = normalizedSeries
|
||||||
};
|
};
|
||||||
|
|
||||||
_scannedSeries.AddOrUpdate(existingKey, new List<ParserInfo>() {info}, (_, oldValue) =>
|
_scannedSeries.AddOrUpdate(existingKey, new List<ParserInfo>() {info}, (_, oldValue) =>
|
||||||
@ -174,8 +182,11 @@ namespace API.Services.Tasks.Scanner
|
|||||||
public string MergeName(ParserInfo info)
|
public string MergeName(ParserInfo info)
|
||||||
{
|
{
|
||||||
var normalizedSeries = Parser.Parser.Normalize(info.Series);
|
var normalizedSeries = Parser.Parser.Normalize(info.Series);
|
||||||
|
var normalizedLocalSeries = Parser.Parser.Normalize(info.LocalizedSeries);
|
||||||
var existingName =
|
var existingName =
|
||||||
_scannedSeries.SingleOrDefault(p => Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries && p.Key.Format == info.Format)
|
_scannedSeries.SingleOrDefault(p =>
|
||||||
|
(Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries ||
|
||||||
|
Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) && p.Key.Format == info.Format)
|
||||||
.Key;
|
.Key;
|
||||||
if (existingName != null && !string.IsNullOrEmpty(existingName.Name))
|
if (existingName != null && !string.IsNullOrEmpty(existingName.Name))
|
||||||
{
|
{
|
||||||
|
@ -457,10 +457,14 @@ public class ScannerService : IScannerService
|
|||||||
if (existingSeries != null) continue;
|
if (existingSeries != null) continue;
|
||||||
|
|
||||||
var s = DbFactory.Series(infos[0].Series);
|
var s = DbFactory.Series(infos[0].Series);
|
||||||
if (!string.IsNullOrEmpty(infos[0].SeriesSort))
|
if (!s.SortNameLocked && !string.IsNullOrEmpty(infos[0].SeriesSort))
|
||||||
{
|
{
|
||||||
s.SortName = infos[0].SeriesSort;
|
s.SortName = infos[0].SeriesSort;
|
||||||
}
|
}
|
||||||
|
if (!s.LocalizedNameLocked && !string.IsNullOrEmpty(infos[0].LocalizedSeries))
|
||||||
|
{
|
||||||
|
s.LocalizedName = infos[0].LocalizedSeries;
|
||||||
|
}
|
||||||
s.Format = key.Format;
|
s.Format = key.Format;
|
||||||
s.LibraryId = library.Id; // We have to manually set this since we aren't adding the series to the Library's series.
|
s.LibraryId = library.Id; // We have to manually set this since we aren't adding the series to the Library's series.
|
||||||
newSeries.Add(s);
|
newSeries.Add(s);
|
||||||
@ -529,6 +533,13 @@ public class ScannerService : IScannerService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsedInfos[0] is not the first volume or chapter. We need to find it
|
||||||
|
var localizedSeries = parsedInfos.Select(p => p.LocalizedSeries).FirstOrDefault(p => !string.IsNullOrEmpty(p));
|
||||||
|
if (!series.LocalizedNameLocked && !string.IsNullOrEmpty(localizedSeries))
|
||||||
|
{
|
||||||
|
series.LocalizedName = localizedSeries;
|
||||||
|
}
|
||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name));
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name));
|
||||||
|
|
||||||
UpdateSeriesMetadata(series, allPeople, allGenres, allTags, library.Type);
|
UpdateSeriesMetadata(series, allPeople, allGenres, allTags, library.Type);
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.2" />
|
<PackageReference Include="Flurl.Http" Version="3.2.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.37.0.45539">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.38.0.46746">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -17,17 +17,20 @@
|
|||||||
|
|
||||||
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="pagination.totalItems > 6 || isMobile; else cardTemplate">
|
<!-- <ng-container *ngIf="utilityService.getActiveBreakpoint() <= Breakpoint.Mobile; else cardTemplate">
|
||||||
<div class="d-flex justify-content-center row g-0 mt-2 mb-2">
|
<div class="d-flex justify-content-center row g-0 mt-2 mb-2">
|
||||||
<div class="col-auto ps-1 pe-1 mt-2 mb-2" *ngFor="let item of items; trackBy:trackByIdentity; index as i">
|
<div class="col-auto ps-1 pe-1 mt-2 mb-2" *ngFor="let item of items; trackBy:trackByIdentity; index as i">
|
||||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<p *ngIf="items.length === 0 && !isLoading">
|
<p *ngIf="items.length === 0 && !isLoading">
|
||||||
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container> -->
|
||||||
|
|
||||||
|
<ng-container [ngTemplateOutlet]="cardTemplate"></ng-container>
|
||||||
|
|
||||||
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'bottom' }"></ng-container>
|
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'bottom' }"></ng-container>
|
||||||
|
|
||||||
@ -38,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p *ngIf="items.length === 0 && !isLoading">
|
<p *ngIf="items.length === 0 && !isLoading">
|
||||||
There is no data
|
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core';
|
import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
||||||
|
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
import { Library } from 'src/app/_models/library';
|
import { Library } from 'src/app/_models/library';
|
||||||
import { Pagination } from 'src/app/_models/pagination';
|
import { Pagination } from 'src/app/_models/pagination';
|
||||||
import { FilterEvent, FilterItem, SeriesFilter, SortField } from 'src/app/_models/series-filter';
|
import { FilterEvent, FilterItem, SeriesFilter, SortField } from 'src/app/_models/series-filter';
|
||||||
@ -9,9 +10,6 @@ import { SeriesService } from 'src/app/_services/series.service';
|
|||||||
|
|
||||||
const FILTER_PAG_REGEX = /[^0-9]/g;
|
const FILTER_PAG_REGEX = /[^0-9]/g;
|
||||||
|
|
||||||
const ANIMATION_SPEED = 300;
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card-detail-layout',
|
selector: 'app-card-detail-layout',
|
||||||
templateUrl: './card-detail-layout.component.html',
|
templateUrl: './card-detail-layout.component.html',
|
||||||
@ -52,9 +50,12 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
private onDestory: Subject<void> = new Subject();
|
private onDestory: Subject<void> = new Subject();
|
||||||
isMobile: boolean = false;
|
|
||||||
|
|
||||||
constructor(private seriesService: SeriesService) {
|
get Breakpoint() {
|
||||||
|
return Breakpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private seriesService: SeriesService, public utilityService: UtilityService) {
|
||||||
this.filter = this.seriesService.createSeriesFilter();
|
this.filter = this.seriesService.createSeriesFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +70,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||||||
if (this.pagination === undefined) {
|
if (this.pagination === undefined) {
|
||||||
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1}
|
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isMobile = window.innerWidth <= 480;
|
|
||||||
window.onresize = () => this.isMobile = window.innerWidth <= 480;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@ -83,7 +83,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
processNotificationProgressEvent(event: Message<NotificationProgressEvent>) {
|
processNotificationProgressEvent(event: Message<NotificationProgressEvent>) {
|
||||||
const message = event.payload as NotificationProgressEvent;
|
const message = event.payload as NotificationProgressEvent;
|
||||||
let data;
|
let data;
|
||||||
|
let index = -1;
|
||||||
switch (event.payload.eventType) {
|
switch (event.payload.eventType) {
|
||||||
case 'single':
|
case 'single':
|
||||||
const values = this.singleUpdateSource.getValue();
|
const values = this.singleUpdateSource.getValue();
|
||||||
@ -92,20 +92,12 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
this.activeEvents += 1;
|
this.activeEvents += 1;
|
||||||
break;
|
break;
|
||||||
case 'started':
|
case 'started':
|
||||||
data = this.progressEventsSource.getValue();
|
// Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then.
|
||||||
data.push(message);
|
data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message);
|
||||||
this.progressEventsSource.next(data);
|
this.progressEventsSource.next(data);
|
||||||
this.activeEvents += 1;
|
|
||||||
break;
|
break;
|
||||||
case 'updated':
|
case 'updated':
|
||||||
data = this.progressEventsSource.getValue();
|
data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message);
|
||||||
const index = data.findIndex(m => m.name === message.name);
|
|
||||||
if (index < 0) {
|
|
||||||
data.push(message);
|
|
||||||
this.activeEvents += 1;
|
|
||||||
} else {
|
|
||||||
data[index] = message;
|
|
||||||
}
|
|
||||||
this.progressEventsSource.next(data);
|
this.progressEventsSource.next(data);
|
||||||
break;
|
break;
|
||||||
case 'ended':
|
case 'ended':
|
||||||
@ -119,6 +111,18 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mergeOrUpdate(data: NotificationProgressEvent[], message: NotificationProgressEvent) {
|
||||||
|
const index = data.findIndex(m => m.name === message.name);
|
||||||
|
// Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then.
|
||||||
|
if (index < 0) {
|
||||||
|
data.push(message);
|
||||||
|
this.activeEvents += 1;
|
||||||
|
} else {
|
||||||
|
data[index] = message;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
handleUpdateAvailableClick(message: NotificationProgressEvent) {
|
handleUpdateAvailableClick(message: NotificationProgressEvent) {
|
||||||
if (this.updateNotificationModalRef != null) { return; }
|
if (this.updateNotificationModalRef != null) { return; }
|
||||||
|
@ -258,12 +258,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
backgroundColor: string = '#FFFFFF';
|
backgroundColor: string = '#FFFFFF';
|
||||||
|
|
||||||
|
getPageUrl = (pageNum: number) => {
|
||||||
|
if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum);
|
||||||
|
return this.readerService.getPageUrl(this.chapterId, pageNum);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
|
|
||||||
//getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum);
|
|
||||||
|
|
||||||
get PageNumber() {
|
get PageNumber() {
|
||||||
return Math.max(Math.min(this.pageNum, this.maxPages - 1), 0);
|
return Math.max(Math.min(this.pageNum, this.maxPages - 1), 0);
|
||||||
}
|
}
|
||||||
@ -1096,10 +1097,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
//console.log('cachedImages: ', this.cachedImages.arr.map(img => this.readerService.imageUrlToPageNum(img.src) + ': ' + img.complete));
|
//console.log('cachedImages: ', this.cachedImages.arr.map(img => this.readerService.imageUrlToPageNum(img.src) + ': ' + img.complete));
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageUrl(pageNum: number) {
|
|
||||||
if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum);
|
|
||||||
return this.readerService.getPageUrl(this.chapterId, pageNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPage() {
|
loadPage() {
|
||||||
if (!this.canvas || !this.ctx) { return; }
|
if (!this.canvas || !this.ctx) { return; }
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
|
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
|
||||||
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
|
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
|
||||||
|
|
||||||
<app-tag-badge title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})" [fillStyle]="seriesMetadata.maxCount != 0 && seriesMetadata.maxCount >= seriesMetadata.totalCount ? 'filled' : 'outline'" a11y-click="13,32" class="col-auto"
|
<app-tag-badge title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})" [fillStyle]="seriesMetadata.maxCount != 0 && seriesMetadata.totalCount != 0 && seriesMetadata.maxCount >= seriesMetadata.totalCount ? 'filled' : 'outline'" a11y-click="13,32" class="col-auto"
|
||||||
(click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)"
|
(click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||||
[selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
|
[selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user