aboutsummaryrefslogtreecommitdiff
path: root/Emby.Naming/AudioBook
diff options
context:
space:
mode:
authorGreenback <jimcartlidge@yahoo.co.uk>2020-11-19 18:27:18 +0000
committerGreenback <jimcartlidge@yahoo.co.uk>2020-11-19 18:27:18 +0000
commita3e47f3e4eed60b227359e8ae59c8a6659a1942c (patch)
treee5af2d5c0e658505f397ed48abc4e5870f3857d7 /Emby.Naming/AudioBook
parent60a6627140a83408b8157b9543e62ff48918ef7a (diff)
parente71ab2afb1fda7521c61f860654fd94e3bd5e3e8 (diff)
Updated to latest Unstable.
Diffstat (limited to 'Emby.Naming/AudioBook')
-rw-r--r--Emby.Naming/AudioBook/AudioBookFileInfo.cs23
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParser.cs17
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs14
-rw-r--r--Emby.Naming/AudioBook/AudioBookInfo.cs15
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs129
-rw-r--r--Emby.Naming/AudioBook/AudioBookNameParser.cs67
-rw-r--r--Emby.Naming/AudioBook/AudioBookNameParserResult.cs18
-rw-r--r--Emby.Naming/AudioBook/AudioBookResolver.cs39
8 files changed, 270 insertions, 52 deletions
diff --git a/Emby.Naming/AudioBook/AudioBookFileInfo.cs b/Emby.Naming/AudioBook/AudioBookFileInfo.cs
index c4863b50a..862e39667 100644
--- a/Emby.Naming/AudioBook/AudioBookFileInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookFileInfo.cs
@@ -8,6 +8,21 @@ namespace Emby.Naming.AudioBook
public class AudioBookFileInfo : IComparable<AudioBookFileInfo>
{
/// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookFileInfo"/> class.
+ /// </summary>
+ /// <param name="path">Path to audiobook file.</param>
+ /// <param name="container">File type.</param>
+ /// <param name="partNumber">Number of part this file represents.</param>
+ /// <param name="chapterNumber">Number of chapter this file represents.</param>
+ public AudioBookFileInfo(string path, string container, int? partNumber = default, int? chapterNumber = default)
+ {
+ Path = path;
+ Container = container;
+ PartNumber = partNumber;
+ ChapterNumber = chapterNumber;
+ }
+
+ /// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
@@ -31,14 +46,8 @@ namespace Emby.Naming.AudioBook
/// <value>The chapter number.</value>
public int? ChapterNumber { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether this instance is a directory.
- /// </summary>
- /// <value>The type.</value>
- public bool IsDirectory { get; set; }
-
/// <inheritdoc />
- public int CompareTo(AudioBookFileInfo other)
+ public int CompareTo(AudioBookFileInfo? other)
{
if (ReferenceEquals(this, other))
{
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
index 14edd6492..7b4429ab1 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@@ -8,15 +5,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
+ /// <summary>
+ /// Parser class to extract part and/or chapter number from audiobook filename.
+ /// </summary>
public class AudioBookFilePathParser
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookFilePathParser"/> class.
+ /// </summary>
+ /// <param name="options">Naming options containing AudioBookPartsExpressions.</param>
public AudioBookFilePathParser(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Based on regex determines if filename includes part/chapter number.
+ /// </summary>
+ /// <param name="path">Path to audiobook file.</param>
+ /// <returns>Returns <see cref="AudioBookFilePathParser"/> object.</returns>
public AudioBookFilePathParserResult Parse(string path)
{
AudioBookFilePathParserResult result = default;
@@ -52,8 +61,6 @@ namespace Emby.Naming.AudioBook
}
}
- result.Success = result.ChapterNumber.HasValue || result.PartNumber.HasValue;
-
return result;
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
index 7bfc4479d..48ab8b57d 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
@@ -1,14 +1,18 @@
-#nullable enable
-#pragma warning disable CS1591
-
namespace Emby.Naming.AudioBook
{
+ /// <summary>
+ /// Data object for passing result of audiobook part/chapter extraction.
+ /// </summary>
public struct AudioBookFilePathParserResult
{
+ /// <summary>
+ /// Gets or sets optional number of path extracted from audiobook filename.
+ /// </summary>
public int? PartNumber { get; set; }
+ /// <summary>
+ /// Gets or sets optional number of chapter extracted from audiobook filename.
+ /// </summary>
public int? ChapterNumber { get; set; }
-
- public bool Success { get; set; }
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs
index b0b5bd881..adf403ab6 100644
--- a/Emby.Naming/AudioBook/AudioBookInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookInfo.cs
@@ -10,11 +10,18 @@ namespace Emby.Naming.AudioBook
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookInfo" /> class.
/// </summary>
- public AudioBookInfo()
+ /// <param name="name">Name of audiobook.</param>
+ /// <param name="year">Year of audiobook release.</param>
+ /// <param name="files">List of files composing the actual audiobook.</param>
+ /// <param name="extras">List of extra files.</param>
+ /// <param name="alternateVersions">Alternative version of files.</param>
+ public AudioBookInfo(string name, int? year, List<AudioBookFileInfo>? files, List<AudioBookFileInfo>? extras, List<AudioBookFileInfo>? alternateVersions)
{
- Files = new List<AudioBookFileInfo>();
- Extras = new List<AudioBookFileInfo>();
- AlternateVersions = new List<AudioBookFileInfo>();
+ Name = name;
+ Year = year;
+ Files = files ?? new List<AudioBookFileInfo>();
+ Extras = extras ?? new List<AudioBookFileInfo>();
+ AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>();
}
/// <summary>
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index f4ba11a0d..e9ea9b7a5 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -1,6 +1,6 @@
-#pragma warning disable CS1591
-
+using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@@ -8,40 +8,145 @@ using MediaBrowser.Model.IO;
namespace Emby.Naming.AudioBook
{
+ /// <summary>
+ /// Class used to resolve Name, Year, alternative files and extras from stack of files.
+ /// </summary>
public class AudioBookListResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
+ /// </summary>
+ /// <param name="options">Naming options passed along to <see cref="AudioBookResolver"/> and <see cref="AudioBookNameParser"/>.</param>
public AudioBookListResolver(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Resolves Name, Year and differentiate alternative files and extras from regular audiobook files.
+ /// </summary>
+ /// <param name="files">List of files related to audiobook.</param>
+ /// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
{
var audioBookResolver = new AudioBookResolver(_options);
+ // File with empty fullname will be sorted out here.
var audiobookFileInfos = files
- .Select(i => audioBookResolver.Resolve(i.FullName, i.IsDirectory))
- .Where(i => i != null)
+ .Select(i => audioBookResolver.Resolve(i.FullName))
+ .OfType<AudioBookFileInfo>()
.ToList();
- // Filter out all extras, otherwise they could cause stacks to not be resolved
- // See the unit test TestStackedWithTrailer
- var metadata = audiobookFileInfos
- .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
-
var stackResult = new StackResolver(_options)
- .ResolveAudioBooks(metadata);
+ .ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
- var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList();
+ var stackFiles = stack.Files
+ .Select(i => audioBookResolver.Resolve(i))
+ .OfType<AudioBookFileInfo>()
+ .ToList();
+
stackFiles.Sort();
- var info = new AudioBookInfo { Files = stackFiles, Name = stack.Name };
+
+ var nameParserResult = new AudioBookNameParser(_options).Parse(stack.Name);
+
+ FindExtraAndAlternativeFiles(ref stackFiles, out var extras, out var alternativeVersions, nameParserResult);
+
+ var info = new AudioBookInfo(
+ nameParserResult.Name,
+ nameParserResult.Year,
+ stackFiles,
+ extras,
+ alternativeVersions);
yield return info;
}
}
+
+ private void FindExtraAndAlternativeFiles(ref List<AudioBookFileInfo> stackFiles, out List<AudioBookFileInfo> extras, out List<AudioBookFileInfo> alternativeVersions, AudioBookNameParserResult nameParserResult)
+ {
+ extras = new List<AudioBookFileInfo>();
+ alternativeVersions = new List<AudioBookFileInfo>();
+
+ var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
+ var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
+ var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
+
+ foreach (var group in groupedBy)
+ {
+ if (group.Key.ChapterNumber == null && group.Key.PartNumber == null)
+ {
+ if (group.Count() > 1 || haveChaptersOrPages)
+ {
+ var ex = new List<AudioBookFileInfo>();
+ var alt = new List<AudioBookFileInfo>();
+
+ foreach (var audioFile in group)
+ {
+ var name = Path.GetFileNameWithoutExtension(audioFile.Path);
+ if (name.Equals("audiobook") ||
+ name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
+ name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
+ {
+ alt.Add(audioFile);
+ }
+ else
+ {
+ ex.Add(audioFile);
+ }
+ }
+
+ if (ex.Count > 0)
+ {
+ var extra = ex
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .ToList();
+
+ stackFiles = stackFiles.Except(extra).ToList();
+ extras.AddRange(extra);
+ }
+
+ if (alt.Count > 0)
+ {
+ var alternatives = alt
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .ToList();
+
+ var main = FindMainAudioBookFile(alternatives, nameParserResult.Name);
+ alternatives.Remove(main);
+ stackFiles = stackFiles.Except(alternatives).ToList();
+ alternativeVersions.AddRange(alternatives);
+ }
+ }
+ }
+ else if (group.Count() > 1)
+ {
+ var alternatives = group
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .Skip(1)
+ .ToList();
+
+ stackFiles = stackFiles.Except(alternatives).ToList();
+ alternativeVersions.AddRange(alternatives);
+ }
+ }
+ }
+
+ private AudioBookFileInfo FindMainAudioBookFile(List<AudioBookFileInfo> files, string name)
+ {
+ var main = files.Find(x => Path.GetFileNameWithoutExtension(x.Path).Equals(name, StringComparison.OrdinalIgnoreCase));
+ main ??= files.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x.Path).Equals("audiobook", StringComparison.OrdinalIgnoreCase));
+ main ??= files.OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .First();
+
+ return main;
+ }
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs
new file mode 100644
index 000000000..120482bc2
--- /dev/null
+++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs
@@ -0,0 +1,67 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.AudioBook
+{
+ /// <summary>
+ /// Helper class to retrieve name and year from audiobook previously retrieved name.
+ /// </summary>
+ public class AudioBookNameParser
+ {
+ private readonly NamingOptions _options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookNameParser"/> class.
+ /// </summary>
+ /// <param name="options">Naming options containing AudioBookNamesExpressions.</param>
+ public AudioBookNameParser(NamingOptions options)
+ {
+ _options = options;
+ }
+
+ /// <summary>
+ /// Parse name and year from previously determined name of audiobook.
+ /// </summary>
+ /// <param name="name">Name of the audiobook.</param>
+ /// <returns>Returns <see cref="AudioBookNameParserResult"/> object.</returns>
+ 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 000000000..3f2d7b2b0
--- /dev/null
+++ b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
@@ -0,0 +1,18 @@
+namespace Emby.Naming.AudioBook
+{
+ /// <summary>
+ /// Data object used to pass result of name and year parsing.
+ /// </summary>
+ public struct AudioBookNameParserResult
+ {
+ /// <summary>
+ /// Gets or sets name of audiobook.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets optional year of release.
+ /// </summary>
+ public int? Year { get; set; }
+ }
+}
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index 5807d4688..f6ad3601d 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -8,25 +5,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
+ /// <summary>
+ /// Resolve specifics (path, container, partNumber, chapterNumber) about audiobook file.
+ /// </summary>
public class AudioBookResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookResolver"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> containing AudioFileExtensions and also used to pass to AudioBookFilePathParser.</param>
public AudioBookResolver(NamingOptions options)
{
_options = options;
}
- public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
+ /// <summary>
+ /// Resolve specifics (path, container, partNumber, chapterNumber) about audiobook file.
+ /// </summary>
+ /// <param name="path">Path to audiobook file.</param>
+ /// <returns>Returns <see cref="AudioBookResolver"/> object.</returns>
+ public AudioBookFileInfo? Resolve(string path)
{
- if (path.Length == 0)
- {
- throw new ArgumentException("String can't be empty.", nameof(path));
- }
-
- // TODO
- if (isDirectory)
+ if (path.Length == 0 || Path.GetFileNameWithoutExtension(path).Length == 0)
{
+ // Return null to indicate this path will not be used, instead of stopping whole process with exception
return null;
}
@@ -42,14 +46,11 @@ namespace Emby.Naming.AudioBook
var parsingResult = new AudioBookFilePathParser(_options).Parse(path);
- return new AudioBookFileInfo
- {
- Path = path,
- Container = container,
- ChapterNumber = parsingResult.ChapterNumber,
- PartNumber = parsingResult.PartNumber,
- IsDirectory = isDirectory
- };
+ return new AudioBookFileInfo(
+ path,
+ container,
+ chapterNumber: parsingResult.ChapterNumber,
+ partNumber: parsingResult.PartNumber);
}
}
}