aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBond-009 <bond.009@outlook.com>2026-06-17 20:54:49 +0200
committerGitHub <noreply@github.com>2026-06-17 20:54:49 +0200
commit49f8a96360058419acb14217699f71a2c970e055 (patch)
treeeedfa22ae3c435fd4fab1d46143c54913a5a5112
parent364f1e12c099213f5b0aeaefff4fd9b36fdb1c8c (diff)
parentf4bab458a2d5b5cbc8c70c305e31949436bfefaf (diff)
Merge pull request #17087 from dkanada/book-resolver
improve book resolution from filename
-rw-r--r--Emby.Naming/Book/BookFileNameParser.cs29
-rw-r--r--Emby.Naming/Book/BookFileNameParserResult.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs8
-rw-r--r--tests/Jellyfin.Naming.Tests/Book/BookResolverTests.cs58
4 files changed, 92 insertions, 11 deletions
diff --git a/Emby.Naming/Book/BookFileNameParser.cs b/Emby.Naming/Book/BookFileNameParser.cs
index 28625f16de..080e25969b 100644
--- a/Emby.Naming/Book/BookFileNameParser.cs
+++ b/Emby.Naming/Book/BookFileNameParser.cs
@@ -1,3 +1,4 @@
+using System;
using System.Text.RegularExpressions;
namespace Emby.Naming.Book
@@ -5,7 +6,7 @@ namespace Emby.Naming.Book
/// <summary>
/// Helper class to retrieve basic metadata from a book filename.
/// </summary>
- public static class BookFileNameParser
+ public static partial class BookFileNameParser
{
private const string NameMatchGroup = "name";
private const string IndexMatchGroup = "index";
@@ -15,14 +16,17 @@ namespace Emby.Naming.Book
private static readonly Regex[] _nameMatches =
[
// seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required
- new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"),
- new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)((\s\((?<year>[0-9]{4})\))?)$"),
- new Regex(@"^(?<index>[0-9]+)\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"),
+ new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)(?:\.0)?((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"),
+ new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)(?:\.0)?((\s\((?<year>[0-9]{4})\))?)$"),
+ new Regex(@"^(?<index>[0-9]+)(?:\.0)?\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"(?<name>.*)\((?<year>[0-9]{4})\)"),
// last resort matches the whole string as the name
new Regex(@"(?<name>.*)")
];
+ [GeneratedRegex(@"^(?<name>.+?)(\sv(?<volume>[0-9]+))?(\sc(?<chapter>[0-9]+))?$")]
+ private static partial Regex ComicRegex();
+
/// <summary>
/// Parse a filename name to retrieve the book name, series name, index, and year.
/// </summary>
@@ -48,7 +52,22 @@ namespace Emby.Naming.Book
if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success)
{
- result.Name = nameGroup.Value.Trim();
+ var comicMatch = ComicRegex().Match(nameGroup.Value.Trim());
+
+ if (comicMatch.Success)
+ {
+ if (comicMatch.Groups.TryGetValue("volume", out Group? volumeGroup) && volumeGroup.Success && int.TryParse(volumeGroup.ValueSpan, out var volume))
+ {
+ result.ParentIndex = volume;
+ }
+
+ if (comicMatch.Groups.TryGetValue("chapter", out Group? chapterGroup) && chapterGroup.Success && int.TryParse(chapterGroup.ValueSpan, out var chapter))
+ {
+ result.Index = chapter;
+ }
+ }
+
+ result.Name = nameGroup.ValueSpan.Trim().ToString();
}
if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index))
diff --git a/Emby.Naming/Book/BookFileNameParserResult.cs b/Emby.Naming/Book/BookFileNameParserResult.cs
index f29716b9e3..f313b202c5 100644
--- a/Emby.Naming/Book/BookFileNameParserResult.cs
+++ b/Emby.Naming/Book/BookFileNameParserResult.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace Emby.Naming.Book
{
/// <summary>
@@ -14,6 +12,7 @@ namespace Emby.Naming.Book
{
Name = null;
Index = null;
+ ParentIndex = null;
Year = null;
SeriesName = null;
}
@@ -29,6 +28,11 @@ namespace Emby.Naming.Book
public int? Index { get; set; }
/// <summary>
+ /// Gets or sets the parent index number.
+ /// </summary>
+ public int? ParentIndex { get; set; }
+
+ /// <summary>
/// Gets or sets the publication year.
/// </summary>
public int? Year { get; set; }
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 1e885aad6e..7d51a0daa0 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -18,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
- protected override Book Resolve(ItemResolveArgs args)
+ protected override Book? Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
@@ -47,13 +45,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
Path = args.Path,
Name = result.Name ?? string.Empty,
IndexNumber = result.Index,
+ ParentIndexNumber = result.ParentIndex,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
IsInMixedFolder = true,
};
}
- private Book GetBook(ItemResolveArgs args)
+ private Book? GetBook(ItemResolveArgs args)
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
@@ -78,6 +77,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
Path = bookFiles[0].FullName,
Name = result.Name ?? string.Empty,
IndexNumber = result.Index,
+ ParentIndexNumber = result.ParentIndex,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? string.Empty,
};
diff --git a/tests/Jellyfin.Naming.Tests/Book/BookResolverTests.cs b/tests/Jellyfin.Naming.Tests/Book/BookResolverTests.cs
new file mode 100644
index 0000000000..19ee13cd75
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Book/BookResolverTests.cs
@@ -0,0 +1,58 @@
+using Emby.Naming.Book;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Book;
+
+public class BookResolverTests
+{
+ [Theory]
+ // seriesName (seriesYear?) #index (of count?) (year?)
+ [InlineData("Sherlock Holmes (1887) #1 (of 4) (1887)", null, "Sherlock Holmes", 1, 1887)]
+ [InlineData("Sherlock Holmes #2", null, "Sherlock Holmes", 2, null)]
+ [InlineData("Sherlock Holmes (1887) #1", null, "Sherlock Holmes", 1, null)]
+ [InlineData("Sherlock Holmes #2 (1890)", null, "Sherlock Holmes", 2, 1890)]
+ // name (seriesName, #index) (year?)
+ [InlineData("A Study in Scarlet (Sherlock Holmes, #1) (1887)", "A Study in Scarlet", "Sherlock Holmes", 1, 1887)]
+ [InlineData("The Adventures of Sherlock Holmes (Sherlock Holmes, #5)", "The Adventures of Sherlock Holmes", "Sherlock Holmes", 5, null)]
+ // name (year)
+ [InlineData("The Sign of the Four (1890)", "The Sign of the Four", null, null, 1890)]
+ [InlineData("The Valley of Fear (1915)", "The Valley of Fear", null, null, 1915)]
+ // index - name (year?)
+ [InlineData("2 - The Sign of the Four (1890)", "The Sign of the Four", null, 2, 1890)]
+ [InlineData("4 - The Valley of Fear", "The Valley of Fear", null, 4, null)]
+ // parse entire string as book name
+ [InlineData("A Study in Scarlet", "A Study in Scarlet", null, null, null)]
+ [InlineData("The Adventures of Sherlock Holmes", "The Adventures of Sherlock Holmes", null, null, null)]
+ // leading zeros on index number
+ [InlineData("00 - Dracula's Guest (1914)", "Dracula's Guest", null, 0, 1914)]
+ [InlineData("01 - Dracula (1897)", "Dracula", null, 1, 1897)]
+ // basic decimal support for prequels and novellas
+ [InlineData("2.0 - Twenty Thousand Leagues Under the Sea", "Twenty Thousand Leagues Under the Sea", null, 2, null)]
+ // TODO decide how to process non-zero decimals
+ [InlineData("2.1 - The Blockade Runners", "2.1 - The Blockade Runners", null, null, null)]
+ public void Resolve_Books(string input, string? name, string? series, int? index, int? year)
+ {
+ var result = BookFileNameParser.Parse(input);
+
+ Assert.Equal(name, result.Name);
+ Assert.Equal(series, result.SeriesName);
+ Assert.Equal(index, result.Index);
+ Assert.Equal(year, result.Year);
+ }
+
+ [Theory]
+ // name volume? chapter? (year?)
+ [InlineData("Captain Marvel Adventures v01 (1941)", "Captain Marvel Adventures v01", null, null, 1, 1941)]
+ [InlineData("Captain Marvel Adventures c120", "Captain Marvel Adventures c120", null, 120, null, null)]
+ [InlineData("Captain Marvel Adventures v01 c120", "Captain Marvel Adventures v01 c120", null, 120, 1, null)]
+ public void Resolve_Comics(string input, string? name, string? series, int? chapter, int? volume, int? year)
+ {
+ var result = BookFileNameParser.Parse(input);
+
+ Assert.Equal(name, result.Name);
+ Assert.Equal(series, result.SeriesName);
+ Assert.Equal(chapter, result.Index);
+ Assert.Equal(volume, result.ParentIndex);
+ Assert.Equal(year, result.Year);
+ }
+}