mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-07 18:24:13 -04:00
No More Sort Prefixes (#3895)
This commit is contained in:
parent
9eadf956fb
commit
08c52b4281
178
API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs
Normal file
178
API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
using API.Helpers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Helpers;
|
||||||
|
|
||||||
|
public class BookSortTitlePrefixHelperTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("The Avengers", "Avengers")]
|
||||||
|
[InlineData("A Game of Thrones", "Game of Thrones")]
|
||||||
|
[InlineData("An American Tragedy", "American Tragedy")]
|
||||||
|
public void TestEnglishPrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("El Quijote", "Quijote")]
|
||||||
|
[InlineData("La Casa de Papel", "Casa de Papel")]
|
||||||
|
[InlineData("Los Miserables", "Miserables")]
|
||||||
|
[InlineData("Las Vegas", "Vegas")]
|
||||||
|
[InlineData("Un Mundo Feliz", "Mundo Feliz")]
|
||||||
|
[InlineData("Una Historia", "Historia")]
|
||||||
|
public void TestSpanishPrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Le Petit Prince", "Petit Prince")]
|
||||||
|
[InlineData("La Belle et la Bête", "Belle et la Bête")]
|
||||||
|
[InlineData("Les Misérables", "Misérables")]
|
||||||
|
[InlineData("Un Amour de Swann", "Amour de Swann")]
|
||||||
|
[InlineData("Une Vie", "Vie")]
|
||||||
|
[InlineData("Des Souris et des Hommes", "Souris et des Hommes")]
|
||||||
|
public void TestFrenchPrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Der Herr der Ringe", "Herr der Ringe")]
|
||||||
|
[InlineData("Die Verwandlung", "Verwandlung")]
|
||||||
|
[InlineData("Das Kapital", "Kapital")]
|
||||||
|
[InlineData("Ein Sommernachtstraum", "Sommernachtstraum")]
|
||||||
|
[InlineData("Eine Geschichte", "Geschichte")]
|
||||||
|
public void TestGermanPrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Il Nome della Rosa", "Nome della Rosa")]
|
||||||
|
[InlineData("La Divina Commedia", "Divina Commedia")]
|
||||||
|
[InlineData("Lo Hobbit", "Hobbit")]
|
||||||
|
[InlineData("Gli Ultimi", "Ultimi")]
|
||||||
|
[InlineData("Le Città Invisibili", "Città Invisibili")]
|
||||||
|
[InlineData("Un Giorno", "Giorno")]
|
||||||
|
[InlineData("Una Notte", "Notte")]
|
||||||
|
public void TestItalianPrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("O Alquimista", "Alquimista")]
|
||||||
|
[InlineData("A Moreninha", "Moreninha")]
|
||||||
|
[InlineData("Os Lusíadas", "Lusíadas")]
|
||||||
|
[InlineData("As Meninas", "Meninas")]
|
||||||
|
[InlineData("Um Defeito de Cor", "Defeito de Cor")]
|
||||||
|
[InlineData("Uma História", "História")]
|
||||||
|
public void TestPortuguesePrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "")] // Empty string returns empty
|
||||||
|
[InlineData("Book", "Book")] // Single word, no change
|
||||||
|
[InlineData("Avengers", "Avengers")] // No prefix, no change
|
||||||
|
public void TestNoPrefixCases(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("The", "The")] // Just a prefix word alone
|
||||||
|
[InlineData("A", "A")] // Just single letter prefix alone
|
||||||
|
[InlineData("Le", "Le")] // French prefix alone
|
||||||
|
public void TestPrefixWordAlone(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("THE AVENGERS", "AVENGERS")] // All caps
|
||||||
|
[InlineData("the avengers", "avengers")] // All lowercase
|
||||||
|
[InlineData("The AVENGERS", "AVENGERS")] // Mixed case
|
||||||
|
[InlineData("tHe AvEnGeRs", "AvEnGeRs")] // Random case
|
||||||
|
public void TestCaseInsensitivity(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Then Came You", "Then Came You")] // "The" + "n" = not a prefix
|
||||||
|
[InlineData("And Then There Were None", "And Then There Were None")] // "An" + "d" = not a prefix
|
||||||
|
[InlineData("Elsewhere", "Elsewhere")] // "El" + "sewhere" = not a prefix (no space)
|
||||||
|
[InlineData("Lesson Plans", "Lesson Plans")] // "Les" + "son" = not a prefix (no space)
|
||||||
|
[InlineData("Theory of Everything", "Theory of Everything")] // "The" + "ory" = not a prefix
|
||||||
|
public void TestFalsePositivePrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("The ", "The ")] // Prefix with only space after - returns original
|
||||||
|
[InlineData("La ", "La ")] // Same for other languages
|
||||||
|
[InlineData("El ", "El ")] // Same for Spanish
|
||||||
|
public void TestPrefixWithOnlySpaceAfter(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("The Multiple Spaces", " Multiple Spaces")] // Doesn't trim extra spaces from remainder
|
||||||
|
[InlineData("Le Petit Prince", " Petit Prince")] // Leading space preserved in remainder
|
||||||
|
public void TestSpaceHandling(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("The The Matrix", "The Matrix")] // Removes first "The", leaves second
|
||||||
|
[InlineData("A A Clockwork Orange", "A Clockwork Orange")] // Removes first "A", leaves second
|
||||||
|
[InlineData("El El Cid", "El Cid")] // Spanish version
|
||||||
|
public void TestRepeatedPrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("L'Étranger", "L'Étranger")] // French contraction - no space, no change
|
||||||
|
[InlineData("D'Artagnan", "D'Artagnan")] // Contraction - no space, no change
|
||||||
|
[InlineData("The-Matrix", "The-Matrix")] // Hyphen instead of space - no change
|
||||||
|
[InlineData("The.Avengers", "The.Avengers")] // Period instead of space - no change
|
||||||
|
public void TestNonSpaceSeparators(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("三国演义", "三国演义")] // Chinese - no processing due to CJK detection
|
||||||
|
[InlineData("한국어", "한국어")] // Korean - not in CJK range, would be processed normally
|
||||||
|
public void TestCjkLanguages(string inputString, string expected)
|
||||||
|
{
|
||||||
|
// NOTE: These don't do anything, I am waiting for user input on if these are needed
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("नमस्ते दुनिया", "नमस्ते दुनिया")] // Hindi - not CJK, processed normally
|
||||||
|
[InlineData("مرحبا بالعالم", "مرحبا بالعالم")] // Arabic - not CJK, processed normally
|
||||||
|
[InlineData("שלום עולם", "שלום עולם")] // Hebrew - not CJK, processed normally
|
||||||
|
public void TestNonLatinNonCjkScripts(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("в мире", "мире")] // Russian "в" (in) - should be removed
|
||||||
|
[InlineData("на столе", "столе")] // Russian "на" (on) - should be removed
|
||||||
|
[InlineData("с друзьями", "друзьями")] // Russian "с" (with) - should be removed
|
||||||
|
public void TestRussianPrefixes(string inputString, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
|
||||||
|
}
|
||||||
|
}
|
@ -972,4 +972,27 @@ public class ScannerServiceTests : AbstractDbTest
|
|||||||
Assert.Contains(postLib.Series, x => x.Name == "Immoral Guild");
|
Assert.Contains(postLib.Series, x => x.Name == "Immoral Guild");
|
||||||
Assert.Contains(postLib.Series, x => x.Name == "Futoku No Guild");
|
Assert.Contains(postLib.Series, x => x.Name == "Futoku No Guild");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanLibrary_SortName_NoPrefix()
|
||||||
|
{
|
||||||
|
const string testcase = "Series with Prefix - Book.json";
|
||||||
|
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
|
|
||||||
|
library.RemovePrefixForSortName = true;
|
||||||
|
UnitOfWork.LibraryRepository.Update(library);
|
||||||
|
await UnitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var scanner = _scannerHelper.CreateServices();
|
||||||
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
|
||||||
|
var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
|
||||||
|
Assert.NotNull(postLib);
|
||||||
|
Assert.Equal(1, postLib.Series.Count);
|
||||||
|
|
||||||
|
Assert.Equal("The Avengers", postLib.Series.First().Name);
|
||||||
|
Assert.Equal("Avengers", postLib.Series.First().SortName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"The Avengers/The Avengers vol 1.pdf"
|
||||||
|
]
|
@ -624,6 +624,8 @@ public class LibraryController : BaseApiController
|
|||||||
library.AllowScrobbling = dto.AllowScrobbling;
|
library.AllowScrobbling = dto.AllowScrobbling;
|
||||||
library.AllowMetadataMatching = dto.AllowMetadataMatching;
|
library.AllowMetadataMatching = dto.AllowMetadataMatching;
|
||||||
library.EnableMetadata = dto.EnableMetadata;
|
library.EnableMetadata = dto.EnableMetadata;
|
||||||
|
library.RemovePrefixForSortName = dto.RemovePrefixForSortName;
|
||||||
|
|
||||||
library.LibraryFileTypes = dto.FileGroupTypes
|
library.LibraryFileTypes = dto.FileGroupTypes
|
||||||
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
|
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
|
||||||
.Distinct()
|
.Distinct()
|
||||||
|
@ -70,4 +70,8 @@ public sealed record LibraryDto
|
|||||||
/// Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF)
|
/// Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableMetadata { get; set; } = true;
|
public bool EnableMetadata { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Should Kavita remove sort articles "The" for the sort name
|
||||||
|
/// </summary>
|
||||||
|
public bool RemovePrefixForSortName { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ public sealed record UpdateLibraryDto
|
|||||||
public bool AllowMetadataMatching { get; init; }
|
public bool AllowMetadataMatching { get; init; }
|
||||||
[Required]
|
[Required]
|
||||||
public bool EnableMetadata { get; init; }
|
public bool EnableMetadata { get; init; }
|
||||||
|
[Required]
|
||||||
|
public bool RemovePrefixForSortName { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What types of files to allow the scanner to pickup
|
/// What types of files to allow the scanner to pickup
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
3724
API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs
generated
Normal file
3724
API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class LibraryRemoveSortPrefix : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "RemovePrefixForSortName",
|
||||||
|
table: "Library",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RemovePrefixForSortName",
|
||||||
|
table: "Library");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1341,6 +1341,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<string>("PrimaryColor")
|
b.Property<string>("PrimaryColor")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("RemovePrefixForSortName")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("SecondaryColor")
|
b.Property<string>("SecondaryColor")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
@ -52,6 +52,10 @@ public class Library : IEntityDate, IHasCoverImage
|
|||||||
/// Should Kavita read metadata files from the library
|
/// Should Kavita read metadata files from the library
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableMetadata { get; set; } = true;
|
public bool EnableMetadata { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Should Kavita remove sort articles "The" for the sort name
|
||||||
|
/// </summary>
|
||||||
|
public bool RemovePrefixForSortName { get; set; } = false;
|
||||||
|
|
||||||
|
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
|
101
API/Helpers/BookSortTitlePrefixHelper.cs
Normal file
101
API/Helpers/BookSortTitlePrefixHelper.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace API.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responsible for parsing book titles "The man on the street" and removing the prefix -> "man on the street".
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This code is performance sensitive</remarks>
|
||||||
|
public static class BookSortTitlePrefixHelper
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, byte> PrefixLookup;
|
||||||
|
private static readonly Dictionary<char, List<string>> PrefixesByFirstChar;
|
||||||
|
|
||||||
|
static BookSortTitlePrefixHelper()
|
||||||
|
{
|
||||||
|
var prefixes = new[]
|
||||||
|
{
|
||||||
|
// English
|
||||||
|
"the", "a", "an",
|
||||||
|
// Spanish
|
||||||
|
"el", "la", "los", "las", "un", "una", "unos", "unas",
|
||||||
|
// French
|
||||||
|
"le", "la", "les", "un", "une", "des",
|
||||||
|
// German
|
||||||
|
"der", "die", "das", "den", "dem", "ein", "eine", "einen", "einer",
|
||||||
|
// Italian
|
||||||
|
"il", "lo", "la", "gli", "le", "un", "uno", "una",
|
||||||
|
// Portuguese
|
||||||
|
"o", "a", "os", "as", "um", "uma", "uns", "umas",
|
||||||
|
// Russian (transliterated common ones)
|
||||||
|
"в", "на", "с", "к", "от", "для",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build lookup structures
|
||||||
|
PrefixLookup = new Dictionary<string, byte>(prefixes.Length, StringComparer.OrdinalIgnoreCase);
|
||||||
|
PrefixesByFirstChar = new Dictionary<char, List<string>>();
|
||||||
|
|
||||||
|
foreach (var prefix in prefixes)
|
||||||
|
{
|
||||||
|
PrefixLookup[prefix] = 1;
|
||||||
|
|
||||||
|
var firstChar = char.ToLowerInvariant(prefix[0]);
|
||||||
|
if (!PrefixesByFirstChar.TryGetValue(firstChar, out var list))
|
||||||
|
{
|
||||||
|
list = [];
|
||||||
|
PrefixesByFirstChar[firstChar] = list;
|
||||||
|
}
|
||||||
|
list.Add(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static ReadOnlySpan<char> GetSortTitle(ReadOnlySpan<char> title)
|
||||||
|
{
|
||||||
|
if (title.IsEmpty) return title;
|
||||||
|
|
||||||
|
// Fast detection of script type by first character
|
||||||
|
var firstChar = title[0];
|
||||||
|
|
||||||
|
// CJK Unicode ranges - no processing needed for most cases
|
||||||
|
if ((firstChar >= 0x4E00 && firstChar <= 0x9FFF) || // CJK Unified
|
||||||
|
(firstChar >= 0x3040 && firstChar <= 0x309F) || // Hiragana
|
||||||
|
(firstChar >= 0x30A0 && firstChar <= 0x30FF)) // Katakana
|
||||||
|
{
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstSpaceIndex = title.IndexOf(' ');
|
||||||
|
if (firstSpaceIndex <= 0) return title;
|
||||||
|
|
||||||
|
var potentialPrefix = title.Slice(0, firstSpaceIndex);
|
||||||
|
|
||||||
|
// Fast path: check if first character could match any prefix
|
||||||
|
firstChar = char.ToLowerInvariant(potentialPrefix[0]);
|
||||||
|
if (!PrefixesByFirstChar.ContainsKey(firstChar))
|
||||||
|
return title;
|
||||||
|
|
||||||
|
// Only do the expensive lookup if first character matches
|
||||||
|
if (PrefixLookup.ContainsKey(potentialPrefix.ToString()))
|
||||||
|
{
|
||||||
|
var remainder = title.Slice(firstSpaceIndex + 1);
|
||||||
|
return remainder.IsEmpty ? title : remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the sort prefix
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetSortTitle(string title)
|
||||||
|
{
|
||||||
|
var result = GetSortTitle(title.AsSpan());
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
}
|
@ -126,13 +126,17 @@ public class ProcessSeries : IProcessSeries
|
|||||||
series.Format = firstParsedInfo.Format;
|
series.Format = firstParsedInfo.Format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var removePrefix = library.RemovePrefixForSortName;
|
||||||
|
var sortName = removePrefix ? BookSortTitlePrefixHelper.GetSortTitle(series.Name) : series.Name;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(series.SortName))
|
if (string.IsNullOrEmpty(series.SortName))
|
||||||
{
|
{
|
||||||
series.SortName = series.Name;
|
series.SortName = sortName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!series.SortNameLocked)
|
if (!series.SortNameLocked)
|
||||||
{
|
{
|
||||||
series.SortName = series.Name;
|
series.SortName = sortName;
|
||||||
if (!string.IsNullOrEmpty(firstParsedInfo.SeriesSort))
|
if (!string.IsNullOrEmpty(firstParsedInfo.SeriesSort))
|
||||||
{
|
{
|
||||||
series.SortName = firstParsedInfo.SeriesSort;
|
series.SortName = firstParsedInfo.SeriesSort;
|
||||||
|
@ -32,6 +32,7 @@ export interface Library {
|
|||||||
allowScrobbling: boolean;
|
allowScrobbling: boolean;
|
||||||
allowMetadataMatching: boolean;
|
allowMetadataMatching: boolean;
|
||||||
enableMetadata: boolean;
|
enableMetadata: boolean;
|
||||||
|
removePrefixForSortName: boolean;
|
||||||
collapseSeriesRelationships: boolean;
|
collapseSeriesRelationships: boolean;
|
||||||
libraryFileTypes: Array<FileTypeGroup>;
|
libraryFileTypes: Array<FileTypeGroup>;
|
||||||
excludePatterns: Array<string>;
|
excludePatterns: Array<string>;
|
||||||
|
@ -127,6 +127,16 @@
|
|||||||
</app-setting-item>
|
</app-setting-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mt-4 mb-4">
|
||||||
|
<app-setting-switch [title]="t('remove-prefix-for-sortname-label')" [subtitle]="t('remove-prefix-for-sortname-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input type="checkbox" id="remove-prefix-for-sortname" role="switch" formControlName="removePrefixForSortName" class="form-check-input">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 mt-4 mb-4">
|
<div class="row g-0 mt-4 mb-4">
|
||||||
<app-setting-switch [title]="t('enable-metadata-label')" [subtitle]="t('enable-metadata-tooltip')">
|
<app-setting-switch [title]="t('enable-metadata-label')" [subtitle]="t('enable-metadata-tooltip')">
|
||||||
<ng-template #switch>
|
<ng-template #switch>
|
||||||
|
@ -115,6 +115,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||||||
allowMetadataMatching: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
|
allowMetadataMatching: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
|
||||||
collapseSeriesRelationships: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
|
collapseSeriesRelationships: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
|
||||||
enableMetadata: new FormControl<boolean>(true, { nonNullable: true, validators: [] }), // required validator doesn't check value, just if true
|
enableMetadata: new FormControl<boolean>(true, { nonNullable: true, validators: [] }), // required validator doesn't check value, just if true
|
||||||
|
removePrefixForSortName: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
selectedFolders: string[] = [];
|
selectedFolders: string[] = [];
|
||||||
@ -273,7 +274,8 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||||||
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false);
|
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false);
|
||||||
this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false);
|
this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false);
|
||||||
this.libraryForm.get('excludePatterns')?.setValue(this.excludePatterns ? this.library.excludePatterns : false);
|
this.libraryForm.get('excludePatterns')?.setValue(this.excludePatterns ? this.library.excludePatterns : false);
|
||||||
this.libraryForm.get('enableMetadata')?.setValue(this.library.enableMetadata, true);
|
this.libraryForm.get('enableMetadata')?.setValue(this.library.enableMetadata);
|
||||||
|
this.libraryForm.get('removePrefixForSortName')?.setValue(this.library.removePrefixForSortName);
|
||||||
this.selectedFolders = this.library.folders;
|
this.selectedFolders = this.library.folders;
|
||||||
|
|
||||||
this.madeChanges = false;
|
this.madeChanges = false;
|
||||||
|
@ -1131,6 +1131,8 @@
|
|||||||
"include-in-search-tooltip": "Should series and any derived information (genres, people, files) from the library be included in search results.",
|
"include-in-search-tooltip": "Should series and any derived information (genres, people, files) from the library be included in search results.",
|
||||||
"enable-metadata-label": "Enable Metadata (ComicInfo/Epub/PDF)",
|
"enable-metadata-label": "Enable Metadata (ComicInfo/Epub/PDF)",
|
||||||
"enable-metadata-tooltip": "Allow Kavita to read metadata files which override filename parsing.",
|
"enable-metadata-tooltip": "Allow Kavita to read metadata files which override filename parsing.",
|
||||||
|
"remove-prefix-for-sortname-label": "Remove common prefixes for Sort Name",
|
||||||
|
"remove-prefix-for-sortname-tooltip": "Kavita will remove common prefixes like 'The', 'A', 'An' from titles for sort name. Does not override set metadata.",
|
||||||
"force-scan": "Force Scan",
|
"force-scan": "Force Scan",
|
||||||
"force-scan-tooltip": "This will force a scan on the library, treating like a fresh scan",
|
"force-scan-tooltip": "This will force a scan on the library, treating like a fresh scan",
|
||||||
"reset": "{{common.reset}}",
|
"reset": "{{common.reset}}",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user