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)