diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs
index fba11ea726..353a0f4a01 100644
--- a/Emby.Naming/AudioBook/AudioBookInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookInfo.cs
@@ -11,12 +11,14 @@ namespace Emby.Naming.AudioBook
/// Initializes a new instance of the class.
///
/// Name of audiobook.
- public AudioBookInfo(string name)
+ /// Year of audiobook release.
+ public AudioBookInfo(string name, int? year)
{
Files = new List();
Extras = new List();
AlternateVersions = new List();
Name = name;
+ Year = year;
}
///
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index 795065a6c9..86ba2eeeaf 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -41,9 +41,9 @@ namespace Emby.Naming.AudioBook
stackFiles.Sort();
- // stack.Name can be empty when we have file without folder, but always have some files
- var name = string.IsNullOrEmpty(stack.Name) ? stack.Files[0] : stack.Name;
- var info = new AudioBookInfo(name) { Files = stackFiles };
+ var result = new AudioBookNameParser(_options).Parse(stack.Name);
+
+ var info = new AudioBookInfo(result.Name, result.Year) { Files = stackFiles };
yield return info;
}
diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs
new file mode 100644
index 0000000000..c48db93b37
--- /dev/null
+++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs
@@ -0,0 +1,59 @@
+#nullable enable
+#pragma warning disable CS1591
+
+using System.Globalization;
+using System.IO;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.AudioBook
+{
+ public class AudioBookNameParser
+ {
+ private readonly NamingOptions _options;
+
+ public AudioBookNameParser(NamingOptions options)
+ {
+ _options = options;
+ }
+
+ public AudioBookNameParserResult Parse(string name)
+ {
+ AudioBookNameParserResult result = default;
+ foreach (var expression in _options.AudioBookNamesExpressions)
+ {
+ var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
+ if (match.Success)
+ {
+ if (result.Name == null)
+ {
+ var value = match.Groups["name"];
+ if (value.Success)
+ {
+ result.Name = value.Value;
+ }
+ }
+
+ if (!result.Year.HasValue)
+ {
+ var value = match.Groups["year"];
+ if (value.Success)
+ {
+ if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ {
+ result.Year = intValue;
+ }
+ }
+ }
+ }
+ }
+
+ if (string.IsNullOrEmpty(result.Name))
+ {
+ result.Name = name;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Emby.Naming/AudioBook/AudioBookNameParserResult.cs b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
new file mode 100644
index 0000000000..b28e259dda
--- /dev/null
+++ b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
@@ -0,0 +1,12 @@
+#nullable enable
+#pragma warning disable CS1591
+
+namespace Emby.Naming.AudioBook
+{
+ public struct AudioBookNameParserResult
+ {
+ public string Name { get; set; }
+
+ public int? Year { get; set; }
+ }
+}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 537de63d55..5bf232451b 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -575,6 +575,13 @@ namespace Emby.Naming.Common
@"dis(?:c|k)[\s_-]?(?[0-9]+)"
};
+ AudioBookNamesExpressions = new[]
+ {
+ // Detect year usually in brackets after name Batman (2020)
+ @"^(?.+?)\s*\(\s*(?\d{4})\s*\)\s*$",
+ @"^\s*(?.+?)\s*$"
+ };
+
var extensions = VideoFileExtensions.ToList();
extensions.AddRange(new[]
@@ -658,6 +665,8 @@ namespace Emby.Naming.Common
public string[] AudioBookPartsExpressions { get; set; }
+ public string[] AudioBookNamesExpressions { get; set; }
+
public StubTypeRule[] StubTypes { get; set; }
public char[] VideoFlagDelimiters { get; set; }
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index ce3152739b..e11b4063ce 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -36,13 +36,25 @@ namespace Emby.Naming.Video
foreach (var directory in groupedDirectoryFiles)
{
- var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
- foreach (var file in directory)
+ if (string.IsNullOrEmpty(directory.Key))
{
- stack.Files.Add(file.Path);
+ foreach (var file in directory)
+ {
+ var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
+ stack.Files.Add(file.Path);
+ yield return stack;
+ }
}
+ else
+ {
+ var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
+ foreach (var file in directory)
+ {
+ stack.Files.Add(file.Path);
+ }
- yield return stack;
+ yield return stack;
+ }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
index c4b061b4e9..91492d46c9 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
@@ -72,33 +73,69 @@ namespace Jellyfin.Naming.Tests.AudioBook
}
[Fact]
- public void TestYearExtraction()
+ public void TestNameYearExtraction()
{
- var files = new[]
+ var data = new[]
{
- "Harry Potter and the Deathly Hallows (2007)/Chapter 1.ogg",
- "Harry Potter and the Deathly Hallows (2007)/Chapter 2.mp3",
-
- "Batman (2020).ogg",
-
- "Batman(2021).mp3",
-
- "Batman.mp3"
+ new NameYearPath
+ {
+ Name = "Harry Potter and the Deathly Hallows",
+ Path = "Harry Potter and the Deathly Hallows (2007)/Chapter 1.ogg",
+ Year = 2007
+ },
+ new NameYearPath
+ {
+ Name = "Batman",
+ Path = "Batman (2020).ogg",
+ Year = 2020
+ },
+ new NameYearPath
+ {
+ Name = "Batman",
+ Path = "Batman( 2021 ).mp3",
+ Year = 2021
+ },
+ new NameYearPath
+ {
+ Name = "Batman(*2021*)",
+ Path = "Batman(*2021*).mp3",
+ Year = null
+ },
+ new NameYearPath
+ {
+ Name = "Batman",
+ Path = "Batman.mp3",
+ Year = null
+ },
+ new NameYearPath
+ {
+ Name = "+ Batman .",
+ Path = " + Batman . .mp3",
+ Year = null
+ },
+ new NameYearPath
+ {
+ Name = " ",
+ Path = " .mp3",
+ Year = null
+ }
};
var resolver = GetResolver();
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ var result = resolver.Resolve(data.Select(i => new FileSystemMetadata
{
IsDirectory = false,
- FullName = i
+ FullName = i.Path
})).ToList();
- Assert.Equal(3, result[0].Files.Count);
- Assert.Equal(2007, result[0].Year);
- Assert.Equal(2020, result[1].Year);
- Assert.Equal(2021, result[2].Year);
- Assert.Null(result[2].Year);
+ Assert.Equal(data.Length, result.Count);
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ Assert.Equal(data[i].Name, result[i].Name);
+ Assert.Equal(data[i].Year, result[i].Year);
+ }
}
[Fact]
@@ -180,5 +217,12 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
return new AudioBookListResolver(_namingOptions);
}
+
+ internal struct NameYearPath
+ {
+ public string Name;
+ public string Path;
+ public int? Year;
+ }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index 5e9d12970a..b3257ace3b 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -35,7 +35,6 @@ namespace Jellyfin.Naming.Tests.AudioBook
};
}
-
[Theory]
[MemberData(nameof(GetResolveFileTestData))]
public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult)