Merge branch 'develop' into feature/oidc
This commit is contained in:
commit
6cac60342b
28 changed files with 4714 additions and 97 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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}"
|
||||
}
|
||||
|
|
|
|||
125
API/I18N/sk.json
125
API/I18N/sk.json
|
|
@ -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}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue