Merge branch 'develop' into feature/oidc

This commit is contained in:
Amelia 2025-07-06 16:43:07 +02:00
commit 6cac60342b
28 changed files with 4714 additions and 97 deletions

View file

@ -624,6 +624,8 @@ public class LibraryController : BaseApiController
library.AllowScrobbling = dto.AllowScrobbling;
library.AllowMetadataMatching = dto.AllowMetadataMatching;
library.EnableMetadata = dto.EnableMetadata;
library.RemovePrefixForSortName = dto.RemovePrefixForSortName;
library.LibraryFileTypes = dto.FileGroupTypes
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
.Distinct()

View file

@ -70,4 +70,8 @@ public sealed record LibraryDto
/// Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF)
/// </summary>
public bool EnableMetadata { get; set; } = true;
/// <summary>
/// Should Kavita remove sort articles "The" for the sort name
/// </summary>
public bool RemovePrefixForSortName { get; set; } = false;
}

View file

@ -30,6 +30,8 @@ public sealed record UpdateLibraryDto
public bool AllowMetadataMatching { get; init; }
[Required]
public bool EnableMetadata { get; init; }
[Required]
public bool RemovePrefixForSortName { get; init; }
/// <summary>
/// What types of files to allow the scanner to pickup
/// </summary>

File diff suppressed because it is too large Load diff

View file

@ -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");
}
}
}

View file

@ -1347,6 +1347,9 @@ namespace API.Data.Migrations
b.Property<string>("PrimaryColor")
.HasColumnType("TEXT");
b.Property<bool>("RemovePrefixForSortName")
.HasColumnType("INTEGER");
b.Property<string>("SecondaryColor")
.HasColumnType("TEXT");

View file

@ -52,6 +52,10 @@ public class Library : IEntityDate, IHasCoverImage
/// Should Kavita read metadata files from the library
/// </summary>
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; }

View 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();
}
}

View file

@ -207,5 +207,7 @@
"sidenav-stream-only-delete-smart-filter": "Seuls les flux de filtres intelligents peuvent être supprimés de la SideNav",
"dashboard-stream-only-delete-smart-filter": "Seuls les flux de filtres intelligents peuvent être supprimés du tableau de bord",
"smart-filter-name-required": "Nom du filtre intelligent requis",
"smart-filter-system-name": "Vous ne pouvez pas utiliser le nom d'un flux fourni par le système"
"smart-filter-system-name": "Vous ne pouvez pas utiliser le nom d'un flux fourni par le système",
"aliases-have-overlap": "Un ou plusieurs alias se chevauchent avec d'autres personnes et ne peuvent pas être mis à jour",
"generated-reading-profile-name": "Généré à partir de {0}"
}

View file

@ -1,5 +1,5 @@
{
"disabled-account": "Váš účet je zakázaný. Kontaktujte správcu servera.",
"disabled-account": "Váš účet je deaktivovaný. Kontaktujte správcu servera.",
"register-user": "Niečo sa pokazilo pri registrácii užívateľa",
"confirm-email": "Najprv musíte potvrdiť svoj e-mail",
"locked-out": "Boli ste zamknutí z dôvodu veľkého počtu neúspešných pokusov o prihlásenie. Počkajte 10 minút.",
@ -88,5 +88,126 @@
"generic-device-create": "Pri vytváraní zariadenia sa vyskytla chyba",
"series-doesnt-exist": "Séria neexistuje",
"volume-doesnt-exist": "Zväzok neexistuje",
"library-name-exists": "Názov knižnice už existuje. Prosím, vyberte si pre daný server jedinečný názov."
"library-name-exists": "Názov knižnice už existuje. Prosím, vyberte si pre daný server jedinečný názov.",
"cache-file-find": "Nepodarilo sa nájsť obrázok vo vyrovnávacej pamäti. Znova načítajte a skúste to znova.",
"name-required": "Názov nemôže byť prázdny",
"valid-number": "Musí to byť platné číslo strany",
"duplicate-bookmark": "Duplicitný záznam záložky už existuje",
"reading-list-permission": "Nemáte povolenia na tento zoznam na čítanie alebo zoznam neexistuje",
"reading-list-position": "Nepodarilo sa aktualizovať pozíciu",
"reading-list-updated": "Aktualizované",
"reading-list-item-delete": "Položku(y) sa nepodarilo odstrániť",
"reading-list-deleted": "Zoznam na čítanie bol odstránený",
"generic-reading-list-delete": "Pri odstraňovaní zoznamu na čítanie sa vyskytol problém",
"generic-reading-list-update": "Pri aktualizácii zoznamu na čítanie sa vyskytol problém",
"generic-reading-list-create": "Pri vytváraní zoznamu na čítanie sa vyskytol problém",
"reading-list-doesnt-exist": "Zoznam na čítanie neexistuje",
"series-restricted": "Používateľ nemá prístup k tejto sérii",
"generic-scrobble-hold": "Pri pauznutí funkcie sa vyskytla chyba",
"libraries-restricted": "Používateľ nemá prístup k žiadnym knižniciam",
"no-series": "Nepodarilo sa získať sériu pre knižnicu",
"no-series-collection": "Nepodarilo sa získať sériu pre kolekciu",
"generic-series-delete": "Pri odstraňovaní série sa vyskytol problém",
"generic-series-update": "Pri aktualizácii série sa vyskytla chyba",
"series-updated": "Úspešne aktualizované",
"update-metadata-fail": "Nepodarilo sa aktualizovať metadáta",
"age-restriction-not-applicable": "Bez obmedzenia",
"generic-relationship": "Pri aktualizácii vzťahov sa vyskytol problém",
"job-already-running": "Úloha už beží",
"encode-as-warning": "Nedá sa konvertovať do formátu PNG. Pre obaly použite možnosť Obnoviť obaly. Záložky a favicony sa nedajú spätne zakódovať.",
"ip-address-invalid": "IP adresa „{0}“ je neplatná",
"bookmark-dir-permissions": "Adresár záložiek nemá správne povolenia pre použitie v aplikácii Kavita",
"total-backups": "Celkový počet záloh musí byť medzi 1 a 30",
"total-logs": "Celkový počet protokolov musí byť medzi 1 a 30",
"stats-permission-denied": "Nemáte oprávnenie zobraziť si štatistiky iného používateľa",
"url-not-valid": "URL nevracia platný obrázok alebo vyžaduje autorizáciu",
"url-required": "Na použitie musíte zadať URL adresu",
"generic-cover-series-save": "Obrázok obálky sa nepodarilo uložiť do série",
"generic-cover-collection-save": "Obrázok obálky sa nepodarilo uložiť do kolekcie",
"generic-cover-reading-list-save": "Obrázok obálky sa nepodarilo uložiť do zoznamu na čítanie",
"generic-cover-chapter-save": "Obrázok obálky sa nepodarilo uložiť do kapitoly",
"generic-cover-library-save": "Obrázok obálky sa nepodarilo uložiť do knižnice",
"generic-cover-person-save": "Obrázok obálky sa nepodarilo uložiť k tejto osobe",
"generic-cover-volume-save": "Obrázok obálky sa nepodarilo uložiť do zväzku",
"access-denied": "Nemáte prístup",
"reset-chapter-lock": "Nepodarilo sa resetovať zámok obalu pre kapitolu",
"generic-user-delete": "Používateľa sa nepodarilo odstrániť",
"generic-user-pref": "Pri ukladaní predvolieb sa vyskytol problém",
"opds-disabled": "OPDS nie je na tomto serveri povolený",
"on-deck": "Pokračovať v čítaní",
"browse-on-deck": "Prehliadať pokračovanie v čítaní",
"recently-added": "Nedávno pridané",
"want-to-read": "Chcem čítať",
"browse-want-to-read": "Prehliadať Chcem si prečítať",
"browse-recently-added": "Prehliadať nedávno pridané",
"reading-lists": "Zoznamy na čítanie",
"browse-reading-lists": "Prehliadať podľa zoznamov na čítanie",
"libraries": "Všetky knižnice",
"browse-libraries": "Prehliadať podľa knižníc",
"collections": "Všetky kolekcie",
"browse-collections": "Prehliadať podľa kolekcií",
"more-in-genre": "Viac v žánri {0}",
"browse-more-in-genre": "Prezrite si viac v {0}",
"recently-updated": "Nedávno aktualizované",
"browse-recently-updated": "Prehliadať nedávno aktualizované",
"smart-filters": "Inteligentné filtre",
"external-sources": "Externé zdroje",
"browse-external-sources": "Prehliadať externé zdroje",
"browse-smart-filters": "Prehliadať podľa inteligentných filtrov",
"reading-list-restricted": "Zoznam na čítanie neexistuje alebo k nemu nemáte prístup",
"query-required": "Musíte zadať parameter dopytu",
"search": "Hľadať",
"search-description": "Vyhľadávanie sérií, zbierok alebo zoznamov na čítanie",
"favicon-doesnt-exist": "Favicon neexistuje",
"smart-filter-doesnt-exist": "Inteligentný filter neexistuje",
"smart-filter-already-in-use": "Existuje existujúci stream s týmto inteligentným filtrom",
"dashboard-stream-doesnt-exist": "Stream dashboardu neexistuje",
"sidenav-stream-doesnt-exist": "SideNav Stream neexistuje",
"external-source-already-exists": "Externý zdroj už existuje",
"external-source-required": "Vyžaduje sa kľúč API a Host",
"external-source-doesnt-exist": "Externý zdroj neexistuje",
"external-source-already-in-use": "S týmto externým zdrojom existuje stream",
"sidenav-stream-only-delete-smart-filter": "Z bočného panela SideNav je možné odstrániť iba streamy inteligentných filtrov",
"dashboard-stream-only-delete-smart-filter": "Z ovládacieho panela je možné odstrániť iba streamy inteligentných filtrov",
"smart-filter-name-required": "Názov inteligentného filtra je povinný",
"smart-filter-system-name": "Nemôžete použiť názov streamu poskytnutého systémom",
"not-authenticated": "Používateľ nie je overený",
"unable-to-register-k+": "Licenciu sa nepodarilo zaregistrovať z dôvodu chyby. Kontaktujte podporu Kavita+",
"unable-to-reset-k+": "Licenciu Kavita+ sa nepodarilo resetovať z dôvodu chyby. Kontaktujte podporu Kavita+",
"anilist-cred-expired": "Prihlasovacie údaje AniList vypršali alebo chýbajú",
"scrobble-bad-payload": "Nesprávne údaje od poskytovateľa Scrobblovania",
"theme-doesnt-exist": "Súbor témy chýba alebo je neplatný",
"bad-copy-files-for-download": "Súbory sa nepodarilo skopírovať do dočasného adresára na stiahnutie archívu.",
"generic-create-temp-archive": "Pri vytváraní dočasného archívu sa vyskytla chyba",
"epub-malformed": "Súbor je nesprávne naformátovaný! Nedá sa prečítať.",
"epub-html-missing": "Zodpovedajúci súbor HTML pre túto stránku sa nenašiel",
"collection-tag-title-required": "Názov kolekcie nemôže byť prázdny",
"reading-list-title-required": "Názov zoznamu na čítanie nemôže byť prázdny",
"collection-tag-duplicate": "Kolekcia s týmto názvom už existuje",
"device-duplicate": "Zariadenie s týmto názvom už existuje",
"device-not-created": "Toto zariadenie ešte neexistuje. Najprv ho vytvorte",
"send-to-permission": "Nie je možné odoslať súbory iné ako EPUB alebo PDF na zariadenia, pretože nie sú podporované na Kindle",
"progress-must-exist": "Pokrok musí byť u používateľa k dispozícii",
"reading-list-name-exists": "Zoznam na prečítanie s týmto menom už existuje",
"user-no-access-library-from-series": "Používateľ nemá prístup do knižnice, do ktorej táto séria patrí",
"series-restricted-age-restriction": "Používateľ si nemôže pozrieť túto sériu z dôvodu vekového obmedzenia",
"kavitaplus-restricted": "Toto je obmedzené iba na Kavita+",
"aliases-have-overlap": "Jeden alebo viacero aliasov sa prekrýva s inými osobami, nie je možné ich aktualizovať",
"volume-num": "Zväzok {0}",
"book-num": "Kniha {0}",
"issue-num": "Problém {0}{1}",
"chapter-num": "Kapitola {0}",
"check-updates": "Skontrolovať aktualizácie",
"license-check": "Kontrola licencie",
"process-scrobbling-events": "Udalosti procesu scrobblovania",
"report-stats": "Štatistiky hlásení",
"check-scrobbling-tokens": "Skontrolujte Tokeny Scrobblingu",
"cleanup": "Čistenie",
"process-processed-scrobbling-events": "Spracovať udalosti scrobblovania",
"remove-from-want-to-read": "Upratanie listu Chcem si prečítať",
"scan-libraries": "Skenovanie knižníc",
"kavita+-data-refresh": "Obnovenie údajov Kavita+",
"backup": "Záloha",
"update-yearly-stats": "Aktualizovať ročné štatistiky",
"generated-reading-profile-name": "Vygenerované z {0}"
}

View file

@ -126,13 +126,17 @@ public class ProcessSeries : IProcessSeries
series.Format = firstParsedInfo.Format;
}
var removePrefix = library.RemovePrefixForSortName;
var sortName = removePrefix ? BookSortTitlePrefixHelper.GetSortTitle(series.Name) : series.Name;
if (string.IsNullOrEmpty(series.SortName))
{
series.SortName = series.Name;
series.SortName = sortName;
}
if (!series.SortNameLocked)
{
series.SortName = series.Name;
series.SortName = sortName;
if (!string.IsNullOrEmpty(firstParsedInfo.SeriesSort))
{
series.SortName = firstParsedInfo.SeriesSort;