aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFredrik Lindberg <fli@shapeshifter.se>2021-08-26 20:01:56 +0200
committerFredrik Lindberg <fli@shapeshifter.se>2021-09-13 17:59:33 +0200
commitea439c5ccf7a61157544accd60109afc12dbc2d2 (patch)
tree0da06983169048f5136861bfe10dc423090b0f11
parente15fea5dade9478d9667399eb0c245917d3e1513 (diff)
Improve series name matching
Add a series path resolver that attempts to extract only the series name from a path that contains more information that just the name.
-rw-r--r--Emby.Naming/Common/NamingOptions.cs14
-rw-r--r--Emby.Naming/TV/SeriesInfo.cs29
-rw-r--r--Emby.Naming/TV/SeriesPathParser.cs61
-rw-r--r--Emby.Naming/TV/SeriesPathParserResult.cs19
-rw-r--r--Emby.Naming/TV/SeriesResolver.cs49
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs8
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeriesPathParserTest.cs28
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs28
8 files changed, 233 insertions, 3 deletions
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 915ce42cc..192171a38 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -368,6 +368,20 @@ namespace Emby.Naming.Common
IsOptimistic = true,
IsNamed = true
},
+
+ // Series and season only expression
+ // "the show/season 1", "the show/s01"
+ new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
+ {
+ IsNamed = true
+ },
+
+ // Series and season only expression
+ // "the show S01", "the show season 1"
+ new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
+ {
+ IsNamed = true
+ },
};
EpisodeWithoutSeasonExpressions = new[]
diff --git a/Emby.Naming/TV/SeriesInfo.cs b/Emby.Naming/TV/SeriesInfo.cs
new file mode 100644
index 000000000..5d6cb4bd3
--- /dev/null
+++ b/Emby.Naming/TV/SeriesInfo.cs
@@ -0,0 +1,29 @@
+namespace Emby.Naming.TV
+{
+ /// <summary>
+ /// Holder object for Series information.
+ /// </summary>
+ public class SeriesInfo
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesInfo"/> class.
+ /// </summary>
+ /// <param name="path">Path to the file.</param>
+ public SeriesInfo(string path)
+ {
+ Path = path;
+ }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the series.
+ /// </summary>
+ /// <value>The name of the series.</value>
+ public string? Name { get; set; }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesPathParser.cs b/Emby.Naming/TV/SeriesPathParser.cs
new file mode 100644
index 000000000..a62e5f4d6
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParser.cs
@@ -0,0 +1,61 @@
+using System.Globalization;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+ /// <summary>
+ /// Used to parse information about series from paths containing more information that only the series name.
+ /// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
+ /// </summary>
+ public static class SeriesPathParser
+ {
+ /// <summary>
+ /// Parses information about series from path.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
+ /// <param name="path">Path.</param>
+ /// <returns>Returns <see cref="SeriesPathParserResult"/> object.</returns>
+ public static SeriesPathParserResult Parse(NamingOptions options, string path)
+ {
+ SeriesPathParserResult? result = null;
+
+ foreach (var expression in options.EpisodeExpressions)
+ {
+ var currentResult = Parse(path, expression);
+ if (currentResult.Success)
+ {
+ result = currentResult;
+ break;
+ }
+ }
+
+ if (result != null)
+ {
+ if (!string.IsNullOrEmpty(result.SeriesName))
+ {
+ result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
+ }
+ }
+
+ return result ?? new SeriesPathParserResult();
+ }
+
+ private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
+ {
+ var result = new SeriesPathParserResult();
+
+ var match = expression.Regex.Match(name);
+
+ if (match.Success && match.Groups.Count >= 3)
+ {
+ if (expression.IsNamed)
+ {
+ result.SeriesName = match.Groups["seriesname"].Value;
+ result.Success = !string.IsNullOrEmpty(result.SeriesName) && !string.IsNullOrEmpty(match.Groups["seasonnumber"]?.Value);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesPathParserResult.cs b/Emby.Naming/TV/SeriesPathParserResult.cs
new file mode 100644
index 000000000..44cd2fdfa
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParserResult.cs
@@ -0,0 +1,19 @@
+namespace Emby.Naming.TV
+{
+ /// <summary>
+ /// Holder object for <see cref="SeriesPathParser"/> result.
+ /// </summary>
+ public class SeriesPathParserResult
+ {
+ /// <summary>
+ /// Gets or sets the name of the series.
+ /// </summary>
+ /// <value>The name of the series.</value>
+ public string? SeriesName { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether parsing was successful.
+ /// </summary>
+ public bool Success { get; set; }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
new file mode 100644
index 000000000..156a03c9e
--- /dev/null
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -0,0 +1,49 @@
+using System.IO;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+ /// <summary>
+ /// Used to resolve information about series from path.
+ /// </summary>
+ public static class SeriesResolver
+ {
+ /// <summary>
+ /// Regex that matches strings of at least 2 characters separated by a dot or underscore.
+ /// Used for removing separators between words, i.e turns "The_show" into "The show" while
+ /// preserving namings like "S.H.O.W".
+ /// </summary>
+ private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
+
+ /// <summary>
+ /// Resolve information about series from path.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object passed to <see cref="SeriesPathParser"/>.</param>
+ /// <param name="path">Path to series.</param>
+ /// <returns>SeriesInfo.</returns>
+ public static SeriesInfo Resolve(NamingOptions options, string path)
+ {
+ string seriesName = Path.GetFileName(path);
+
+ SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
+ if (result.Success)
+ {
+ if (!string.IsNullOrEmpty(result.SeriesName))
+ {
+ seriesName = result.SeriesName;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(seriesName))
+ {
+ seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
+ }
+
+ return new SeriesInfo(path)
+ {
+ Name = seriesName
+ };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index a1562abd3..a997584a6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
+ var seriesInfo = Naming.TV.SeriesResolver.Resolve(_libraryManager.GetNamingOptions(), args.Path);
+
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
@@ -64,7 +66,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return new Series
{
Path = args.Path,
- Name = Path.GetFileName(args.Path)
+ Name = seriesInfo.Name
};
}
}
@@ -81,7 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return new Series
{
Path = args.Path,
- Name = Path.GetFileName(args.Path)
+ Name = seriesInfo.Name
};
}
@@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return new Series
{
Path = args.Path,
- Name = Path.GetFileName(args.Path)
+ Name = seriesInfo.Name
};
}
}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeriesPathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/SeriesPathParserTest.cs
new file mode 100644
index 000000000..ceb5f8b73
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/SeriesPathParserTest.cs
@@ -0,0 +1,28 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class SeriesPathParserTest
+ {
+ [Theory]
+ [InlineData("The.Show.S01", "The.Show")]
+ [InlineData("/The.Show.S01", "The.Show")]
+ [InlineData("/some/place/The.Show.S01", "The.Show")]
+ [InlineData("/something/The.Show.S01", "The.Show")]
+ [InlineData("The Show Season 10", "The Show")]
+ [InlineData("The Show S01E01", "The Show")]
+ [InlineData("The Show S01E01 Episode", "The Show")]
+ [InlineData("/something/The Show/Season 1", "The Show")]
+ [InlineData("/something/The Show/S01", "The Show")]
+ public void SeriesPathParserParseTest(string path, string name)
+ {
+ NamingOptions o = new NamingOptions();
+ var res = SeriesPathParser.Parse(o, path);
+
+ Assert.Equal(name, res.SeriesName);
+ Assert.True(res.Success);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs
new file mode 100644
index 000000000..97f4b4058
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs
@@ -0,0 +1,28 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class SeriesResolverTests
+ {
+ [Theory]
+ [InlineData("The.Show.S01", "The Show")]
+ [InlineData("The.Show.S01.COMPLETE", "The Show")]
+ [InlineData("S.H.O.W.S01", "S.H.O.W")]
+ [InlineData("The.Show.P.I.S01", "The Show P.I")]
+ [InlineData("The_Show_Season_1", "The Show")]
+ [InlineData("/something/The_Show/Season 10", "The Show")]
+ [InlineData("The Show", "The Show")]
+ [InlineData("/some/path/The Show", "The Show")]
+ [InlineData("/some/path/The Show s02e10 720p hdtv", "The Show")]
+ [InlineData("/some/path/The Show s02e10 the episode 720p hdtv", "The Show")]
+ public void SeriesResolverResolveTest(string path, string name)
+ {
+ NamingOptions o = new NamingOptions();
+ var res = SeriesResolver.Resolve(o, path);
+
+ Assert.Equal(name, res.Name);
+ }
+ }
+}