aboutsummaryrefslogtreecommitdiff
path: root/Emby.Naming
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Naming')
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs11
-rw-r--r--Emby.Naming/AudioBook/AudioBookResolver.cs4
-rw-r--r--Emby.Naming/Common/NamingOptions.cs90
-rw-r--r--Emby.Naming/Emby.Naming.csproj9
-rw-r--r--Emby.Naming/Subtitles/SubtitleParser.cs11
-rw-r--r--Emby.Naming/TV/EpisodeResolver.cs4
-rw-r--r--Emby.Naming/TV/SeasonPathParser.cs6
-rw-r--r--Emby.Naming/TV/SeriesInfo.cs29
-rw-r--r--Emby.Naming/TV/SeriesPathParser.cs60
-rw-r--r--Emby.Naming/TV/SeriesPathParserResult.cs19
-rw-r--r--Emby.Naming/TV/SeriesResolver.cs49
-rw-r--r--Emby.Naming/Video/CleanStringParser.cs26
-rw-r--r--Emby.Naming/Video/ExtraRuleResolver.cs (renamed from Emby.Naming/Video/ExtraResolver.cs)44
-rw-r--r--Emby.Naming/Video/FileStack.cs29
-rw-r--r--Emby.Naming/Video/FileStackRule.cs48
-rw-r--r--Emby.Naming/Video/Format3DParser.cs2
-rw-r--r--Emby.Naming/Video/StackResolver.cs219
-rw-r--r--Emby.Naming/Video/StubResolver.cs4
-rw-r--r--Emby.Naming/Video/VideoInfo.cs13
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs179
-rw-r--r--Emby.Naming/Video/VideoResolver.cs13
21 files changed, 465 insertions, 404 deletions
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index 1e4a8d2ed..2efe7d526 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
public class AudioBookListResolver
{
private readonly NamingOptions _options;
+ private readonly AudioBookResolver _audioBookResolver;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
public AudioBookListResolver(NamingOptions options)
{
_options = options;
+ _audioBookResolver = new AudioBookResolver(_options);
}
/// <summary>
@@ -31,21 +33,18 @@ namespace Emby.Naming.AudioBook
/// <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))
+ .Select(i => _audioBookResolver.Resolve(i.FullName))
.OfType<AudioBookFileInfo>()
.ToList();
- var stackResult = new StackResolver(_options)
- .ResolveAudioBooks(audiobookFileInfos);
+ var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
var stackFiles = stack.Files
- .Select(i => audioBookResolver.Resolve(i))
+ .Select(i => _audioBookResolver.Resolve(i))
.OfType<AudioBookFileInfo>()
.ToList();
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index f6ad3601d..183b6c3b1 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.AudioBook
{
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
var extension = Path.GetExtension(path);
// Check supported extensions
- if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return null;
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 915ce42cc..c0be0b7c6 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -1,4 +1,7 @@
+#pragma warning disable CA1819
+
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
@@ -122,11 +125,11 @@ namespace Emby.Naming.Common
token: "DSR")
};
- VideoFileStackingExpressions = new[]
+ VideoFileStackingRules = new[]
{
- "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
- "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
- "(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
+ new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
+ new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
+ new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@@ -137,8 +140,11 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
- @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
- @"(\[.*\])"
+ @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+ @"^(?<cleaned>.+?)(\[.*\])",
+ @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
+ @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
+ @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
};
SubtitleFileExtensions = new[]
@@ -250,6 +256,8 @@ namespace Emby.Naming.Common
},
// <!-- foo.ep01, foo.EP_01 -->
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
+ // <!-- foo.E01., foo.e01. -->
+ new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
@@ -368,6 +376,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[]
@@ -384,6 +406,12 @@ namespace Emby.Naming.Common
{
new ExtraRule(
ExtraType.Trailer,
+ ExtraRuleType.DirectoryName,
+ "trailers",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
ExtraRuleType.Filename,
"trailer",
MediaType.Video),
@@ -443,12 +471,24 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
+ ExtraType.ThemeVideo,
+ ExtraRuleType.DirectoryName,
+ "backdrops",
+ MediaType.Video),
+
+ new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.DirectoryName,
+ "theme-music",
+ MediaType.Audio),
+
+ new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
"-scene",
@@ -479,6 +519,12 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.Suffix,
+ "-deletedscene",
+ MediaType.Video),
+
+ new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-featurette",
@@ -536,7 +582,7 @@ namespace Emby.Naming.Common
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extras",
- MediaType.Video),
+ MediaType.Video)
};
Format3DRules = new[]
@@ -648,10 +694,30 @@ namespace Emby.Naming.Common
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
+ AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
+ {
+ ["trailers"] = ExtraType.Trailer,
+ ["theme-music"] = ExtraType.ThemeSong,
+ ["backdrops"] = ExtraType.ThemeVideo,
+ ["extras"] = ExtraType.Unknown,
+ ["behind the scenes"] = ExtraType.BehindTheScenes,
+ ["deleted scenes"] = ExtraType.DeletedScene,
+ ["interviews"] = ExtraType.Interview,
+ ["scenes"] = ExtraType.Scene,
+ ["samples"] = ExtraType.Sample,
+ ["shorts"] = ExtraType.Clip,
+ ["featurettes"] = ExtraType.Clip
+ };
+
Compile();
}
/// <summary>
+ /// Gets or sets the folder name to extra types mapping.
+ /// </summary>
+ public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
+
+ /// <summary>
/// Gets or sets list of audio file extensions.
/// </summary>
public string[] AudioFileExtensions { get; set; }
@@ -732,9 +798,9 @@ namespace Emby.Naming.Common
public Format3DRule[] Format3DRules { get; set; }
/// <summary>
- /// Gets or sets list of raw video file-stacking expressions strings.
+ /// Gets the file stacking rules.
/// </summary>
- public string[] VideoFileStackingExpressions { get; set; }
+ public FileStackRule[] VideoFileStackingRules { get; }
/// <summary>
/// Gets or sets list of raw clean DateTimes regular expressions strings.
@@ -757,11 +823,6 @@ namespace Emby.Naming.Common
public ExtraRule[] VideoExtraRules { get; set; }
/// <summary>
- /// Gets list of video file-stack regular expressions.
- /// </summary>
- public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
-
- /// <summary>
/// Gets list of clean datetime regular expressions.
/// </summary>
public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
@@ -786,7 +847,6 @@ namespace Emby.Naming.Common
/// </summary>
public void Compile()
{
- VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 96f8f389b..433ad137b 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -13,7 +13,10 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
- <AnalysisMode>AllDisabledByDefault</AnalysisMode>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@@ -39,13 +42,13 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
index a19340ef6..5809c512a 100644
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ b/Emby.Naming/Subtitles/SubtitleParser.cs
@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Subtitles
{
@@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
}
var extension = Path.GetExtension(path);
- if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
var flags = GetFlags(path);
var info = new SubtitleInfo(
path,
- _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
- _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
+ _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
+ _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
- var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
- && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
+ var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
+ && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
.ToList();
// Should have a name, language and file extension
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index 5e952e47b..6cebc40c2 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -1,8 +1,8 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Extensions;
namespace Emby.Naming.TV
{
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
{
var extension = Path.GetExtension(path);
// Check supported extensions
- if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
if (!StubResolver.TryResolveFile(path, _options, out stubType))
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index 6236f86c4..fc9ee8e56 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -55,7 +55,7 @@ namespace Emby.Naming.TV
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
/// <returns>System.Nullable{System.Int32}.</returns>
- private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath(
+ private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
string path,
bool supportSpecialAliases,
bool supportNumericSeasonFolders)
@@ -99,7 +99,7 @@ namespace Emby.Naming.TV
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
- if (result.seasonNumber.HasValue)
+ if (result.SeasonNumber.HasValue)
{
return result;
}
@@ -142,7 +142,7 @@ namespace Emby.Naming.TV
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
- private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
+ private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
{
var numericStart = -1;
var length = 0;
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..23067e6a4
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParser.cs
@@ -0,0 +1,60 @@
+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) && !match.Groups["seasonnumber"].ValueSpan.IsEmpty;
+ }
+ }
+
+ 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.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index 4eef3ebc5..a336f8fbd 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
@@ -17,38 +16,39 @@ namespace Emby.Naming.Video
/// <param name="expressions">List of regex to parse name and year from.</param>
/// <param name="newName">Parsing result string.</param>
/// <returns>True if parsing was successful.</returns>
- public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
+ public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out string newName)
{
if (string.IsNullOrEmpty(name))
{
- newName = ReadOnlySpan<char>.Empty;
+ newName = string.Empty;
return false;
}
- var len = expressions.Count;
- for (int i = 0; i < len; i++)
+ // Iteratively apply the regexps to clean the string.
+ bool cleaned = false;
+ for (int i = 0; i < expressions.Count; i++)
{
if (TryClean(name, expressions[i], out newName))
{
- return true;
+ cleaned = true;
+ name = newName;
}
}
- newName = ReadOnlySpan<char>.Empty;
- return false;
+ newName = cleaned ? name : string.Empty;
+ return cleaned;
}
- private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
+ private static bool TryClean(string name, Regex expression, out string newName)
{
var match = expression.Match(name);
- int index = match.Index;
- if (match.Success && index != 0)
+ if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
{
- newName = name.AsSpan().Slice(0, match.Index);
+ newName = cleaned.Value;
return true;
}
- newName = ReadOnlySpan<char>.Empty;
+ newName = string.Empty;
return false;
}
}
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs
index a32af002c..0970e509a 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -9,44 +9,27 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve if file is extra for video.
/// </summary>
- public class ExtraResolver
+ public static class ExtraRuleResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ExtraResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
- public ExtraResolver(NamingOptions options)
- {
- _options = options;
- }
+ private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
/// <summary>
/// Attempts to resolve if file is extra.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
- public ExtraResult GetExtraInfo(string path)
+ public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{
var result = new ExtraResult();
- for (var i = 0; i < _options.VideoExtraRules.Length; i++)
+ for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
{
- var rule = _options.VideoExtraRules[i];
- if (rule.MediaType == MediaType.Audio)
- {
- if (!AudioFileParser.IsAudioFile(path, _options))
- {
- continue;
- }
- }
- else if (rule.MediaType == MediaType.Video)
+ var rule = namingOptions.VideoExtraRules[i];
+ if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
+ || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
{
- if (!VideoResolver.IsVideoFile(path, _options))
- {
- continue;
- }
+ continue;
}
var pathSpan = path.AsSpan();
@@ -62,9 +45,10 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
- var filename = Path.GetFileNameWithoutExtension(pathSpan);
+ // Trim the digits from the end of the filename so we can recognize things like -trailer2
+ var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
- if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+ if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
@@ -74,9 +58,9 @@ namespace Emby.Naming.Video
{
var filename = Path.GetFileName(path);
- var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+ var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
- if (regex.IsMatch(filename))
+ if (isMatch)
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs
index 6519db57c..4902e6728 100644
--- a/Emby.Naming/Video/FileStack.cs
+++ b/Emby.Naming/Video/FileStack.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
/// <summary>
/// Initializes a new instance of the <see cref="FileStack"/> class.
/// </summary>
- public FileStack()
+ /// <param name="name">The stack name.</param>
+ /// <param name="isDirectory">Whether the stack files are directories.</param>
+ /// <param name="files">The stack files.</param>
+ public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
{
- Files = new List<string>();
+ Name = name;
+ IsDirectoryStack = isDirectory;
+ Files = files;
}
/// <summary>
- /// Gets or sets name of file stack.
+ /// Gets the name of file stack.
/// </summary>
- public string Name { get; set; } = string.Empty;
+ public string Name { get; }
/// <summary>
- /// Gets or sets list of paths in stack.
+ /// Gets the list of paths in stack.
/// </summary>
- public List<string> Files { get; set; }
+ public IReadOnlyList<string> Files { get; }
/// <summary>
- /// Gets or sets a value indicating whether stack is directory stack.
+ /// Gets a value indicating whether stack is directory stack.
/// </summary>
- public bool IsDirectoryStack { get; set; }
+ public bool IsDirectoryStack { get; }
/// <summary>
/// Helper function to determine if path is in the stack.
@@ -40,12 +45,12 @@ namespace Emby.Naming.Video
/// <returns>True if file is in the stack.</returns>
public bool ContainsFile(string file, bool isDirectory)
{
- if (IsDirectoryStack == isDirectory)
+ if (string.IsNullOrEmpty(file))
{
- return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
+ return false;
}
- return false;
+ return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs
new file mode 100644
index 000000000..76b487f42
--- /dev/null
+++ b/Emby.Naming/Video/FileStackRule.cs
@@ -0,0 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+
+namespace Emby.Naming.Video;
+
+/// <summary>
+/// Regex based rule for file stacking (eg. disc1, disc2).
+/// </summary>
+public class FileStackRule
+{
+ private readonly Regex _tokenRegex;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FileStackRule"/> class.
+ /// </summary>
+ /// <param name="token">Token.</param>
+ /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
+ public FileStackRule(string token, bool isNumerical)
+ {
+ _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+ IsNumerical = isNumerical;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
+ /// </summary>
+ public bool IsNumerical { get; }
+
+ /// <summary>
+ /// Match the input against the rule regex.
+ /// </summary>
+ /// <param name="input">The input.</param>
+ /// <param name="result">The part type and number or <c>null</c>.</param>
+ /// <returns>A value indicating whether the input matched the rule.</returns>
+ public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
+ {
+ result = null;
+ var match = _tokenRegex.Match(input);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
+ result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
+ return true;
+ }
+}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 089089989..eb5e71d78 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/Emby.Naming/Video/Format3DParser.cs
@@ -9,7 +9,7 @@ namespace Emby.Naming.Video
public static class Format3DParser
{
// Static default result to save on allocation costs.
- private static readonly Format3DResult _defaultResult = new (false, null);
+ private static readonly Format3DResult _defaultResult = new(false, null);
/// <summary>
/// Parse 3D format related flags.
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 36f65a562..8119a0267 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve <see cref="FileStack"/> from list of paths.
/// </summary>
- public class StackResolver
+ public static class StackResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StackResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
- public StackResolver(NamingOptions options)
- {
- _options = options;
- }
-
/// <summary>
/// Resolves only directories from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
- public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
+ public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
}
/// <summary>
/// Resolves only files from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
- public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
+ public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
}
/// <summary>
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
- public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
+ public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
{
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
{
foreach (var file in directory)
{
- var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
- stack.Files.Add(file.Path);
+ var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { 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);
- }
-
+ var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
yield return stack;
}
}
@@ -82,158 +66,91 @@ namespace Emby.Naming.Video
/// Resolves videos from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
- public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
+ public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
{
- var list = files
- .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
- .OrderBy(i => i.FullName)
- .ToList();
-
- var expressions = _options.VideoFileStackingRegexes;
+ var potentialFiles = files
+ .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
+ .OrderBy(i => i.FullName);
- for (var i = 0; i < list.Count; i++)
+ var potentialStacks = new Dictionary<string, StackMetadata>();
+ foreach (var file in potentialFiles)
{
- var offset = 0;
-
- var file1 = list[i];
+ var name = file.Name;
+ if (string.IsNullOrEmpty(name))
+ {
+ name = Path.GetFileName(file.FullName);
+ }
- var expressionIndex = 0;
- while (expressionIndex < expressions.Length)
+ for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
{
- var exp = expressions[expressionIndex];
- var stack = new FileStack();
+ var rule = namingOptions.VideoFileStackingRules[i];
+ if (!rule.Match(name, out var stackParsingResult))
+ {
+ continue;
+ }
- // (Title)(Volume)(Ignore)(Extension)
- var match1 = FindMatch(file1, exp, offset);
+ var stackName = stackParsingResult.Value.StackName;
+ var partNumber = stackParsingResult.Value.PartNumber;
+ var partType = stackParsingResult.Value.PartType;
- if (match1.Success)
+ if (!potentialStacks.TryGetValue(stackName, out var stackResult))
{
- var title1 = match1.Groups["title"].Value;
- var volume1 = match1.Groups["volume"].Value;
- var ignore1 = match1.Groups["ignore"].Value;
- var extension1 = match1.Groups["extension"].Value;
+ stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
+ potentialStacks[stackName] = stackResult;
+ }
- var j = i + 1;
- while (j < list.Count)
+ if (stackResult.Parts.Count > 0)
+ {
+ if (stackResult.IsDirectory != file.IsDirectory
+ || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
+ || stackResult.ContainsPart(partNumber))
{
- var file2 = list[j];
-
- if (file1.IsDirectory != file2.IsDirectory)
- {
- j++;
- continue;
- }
-
- // (Title)(Volume)(Ignore)(Extension)
- var match2 = FindMatch(file2, exp, offset);
-
- if (match2.Success)
- {
- var title2 = match2.Groups[1].Value;
- var volume2 = match2.Groups[2].Value;
- var ignore2 = match2.Groups[3].Value;
- var extension2 = match2.Groups[4].Value;
-
- if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
- {
- if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
- {
- if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
- && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
- {
- if (stack.Files.Count == 0)
- {
- stack.Name = title1 + ignore1;
- stack.IsDirectoryStack = file1.IsDirectory;
- stack.Files.Add(file1.FullName);
- }
-
- stack.Files.Add(file2.FullName);
- }
- else
- {
- // Sequel
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
- {
- // False positive, try again with offset
- offset = match1.Groups[3].Index;
- break;
- }
- else
- {
- // Extension mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // Title mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // No match 2, next expression
- offset = 0;
- expressionIndex++;
- break;
- }
-
- j++;
+ continue;
}
- if (j == list.Count)
+ if (rule.IsNumerical != stackResult.IsNumerical)
{
- expressionIndex = expressions.Length;
+ break;
}
}
- else
- {
- // No match 1
- offset = 0;
- expressionIndex++;
- }
- if (stack.Files.Count > 1)
- {
- yield return stack;
- i += stack.Files.Count - 1;
- break;
- }
+ stackResult.Parts.Add(partNumber, file);
+ break;
}
}
- }
- private static string GetRegexInput(FileSystemMetadata file)
- {
- // For directories, dummy up an extension otherwise the expressions will fail
- var input = !file.IsDirectory
- ? file.FullName
- : file.FullName + ".mkv";
+ foreach (var (fileName, stack) in potentialStacks)
+ {
+ if (stack.Parts.Count < 2)
+ {
+ continue;
+ }
- return Path.GetFileName(input);
+ yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
+ }
}
- private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
+ private class StackMetadata
{
- var regexInput = GetRegexInput(input);
-
- if (offset < 0 || offset >= regexInput.Length)
+ public StackMetadata(bool isDirectory, bool isNumerical, string partType)
{
- return Match.Empty;
+ Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+ IsDirectory = isDirectory;
+ IsNumerical = isNumerical;
+ PartType = partType;
}
- return regex.Match(regexInput, offset);
+ public Dictionary<string, FileSystemMetadata> Parts { get; }
+
+ public bool IsDirectory { get; }
+
+ public bool IsNumerical { get; }
+
+ public string PartType { get; }
+
+ public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
}
}
}
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index 079987fe8..f7ba606e3 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
var extension = Path.GetExtension(path);
- if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs
index 930fdb33f..8847ee9bc 100644
--- a/Emby.Naming/Video/VideoInfo.cs
+++ b/Emby.Naming/Video/VideoInfo.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
Name = name;
Files = Array.Empty<VideoFileInfo>();
- Extras = Array.Empty<VideoFileInfo>();
AlternateVersions = Array.Empty<VideoFileInfo>();
}
@@ -40,15 +40,14 @@ namespace Emby.Naming.Video
public IReadOnlyList<VideoFileInfo> Files { get; set; }
/// <summary>
- /// Gets or sets the extras.
- /// </summary>
- /// <value>The extras.</value>
- public IReadOnlyList<VideoFileInfo> Extras { get; set; }
-
- /// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the extra type.
+ /// </summary>
+ public ExtraType? ExtraType { get; set; }
}
}
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index ed7d511a3..11f82525f 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -17,29 +16,41 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
- /// <param name="files">List of related video files.</param>
+ /// <param name="videoInfos">List of related video files.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
+ /// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
- public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
+ public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
{
- var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
- .OfType<VideoFileInfo>()
- .ToList();
-
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = new StackResolver(namingOptions)
- .Resolve(nonExtras).ToList();
+ var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
+
+ var remainingFiles = new List<VideoFileInfo>();
+ var standaloneMedia = new List<VideoFileInfo>();
- var remainingFiles = videoInfos
- .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
- .ToList();
+ for (var i = 0; i < videoInfos.Count; i++)
+ {
+ var current = videoInfos[i];
+ if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
+ {
+ continue;
+ }
+
+ if (current.ExtraType == null)
+ {
+ standaloneMedia.Add(current);
+ }
+ else
+ {
+ remainingFiles.Add(current);
+ }
+ }
var list = new List<VideoInfo>();
@@ -47,38 +58,20 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
-
- var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
-
- if (extras.Count > 0)
- {
- info.Extras = extras;
- }
-
list.Add(info);
}
- var standaloneMedia = remainingFiles
- .Where(i => i.ExtraType == null)
- .ToList();
-
foreach (var media in standaloneMedia)
{
var info = new VideoInfo(media.Name) { Files = new[] { media } };
info.Year = info.Files[0].Year;
-
- remainingFiles.Remove(media);
- var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
-
- info.Extras = extras;
-
list.Add(info);
}
@@ -87,58 +80,12 @@ namespace Emby.Naming.Video
list = GetVideosGroupedByVersion(list, namingOptions);
}
- // If there's only one resolved video, use the folder name as well to find extras
- if (list.Count == 1)
- {
- var info = list[0];
- var videoPath = list[0].Files[0].Path;
- var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
-
- if (!parentPath.IsEmpty)
- {
- var folderName = Path.GetFileName(parentPath);
- if (!folderName.IsEmpty)
- {
- var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
- extras.AddRange(info.Extras);
- info.Extras = extras;
- }
- }
-
- // Add the extras that are just based on file name as well
- var extrasByFileName = remainingFiles
- .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
- .ToList();
-
- remainingFiles = remainingFiles
- .Except(extrasByFileName)
- .ToList();
-
- extrasByFileName.AddRange(info.Extras);
- info.Extras = extrasByFileName;
- }
-
- // If there's only one video, accept all trailers
- // Be lenient because people use all kinds of mishmash conventions with trailers.
- if (list.Count == 1)
- {
- var trailers = remainingFiles
- .Where(i => i.ExtraType == ExtraType.Trailer)
- .ToList();
-
- trailers.AddRange(list[0].Extras);
- list[0].Extras = trailers;
-
- remainingFiles = remainingFiles
- .Except(trailers)
- .ToList();
- }
-
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
Files = new[] { i },
- Year = i.Year
+ Year = i.Year,
+ ExtraType = i.ExtraType
}));
return list;
@@ -162,6 +109,11 @@ namespace Emby.Naming.Video
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
+ if (video.ExtraType != null)
+ {
+ continue;
+ }
+
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
return videos;
@@ -178,17 +130,14 @@ namespace Emby.Naming.Video
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- var extras = new List<VideoFileInfo>(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
- extras.AddRange(video.Extras);
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString();
- list[0].Extras = extras;
return list;
}
@@ -230,7 +179,7 @@ namespace Emby.Naming.Video
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{
- tmpTestFilename = cleanName.Trim().ToString();
+ tmpTestFilename = cleanName.Trim();
}
// The CleanStringParser should have removed common keywords etc.
@@ -238,67 +187,5 @@ namespace Emby.Naming.Video
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
-
- private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
- {
- return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
- }
-
- private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
- {
- if (baseName.IsEmpty)
- {
- return false;
- }
-
- return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
- || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
- }
-
- /// <summary>
- /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
- /// </summary>
- /// <param name="remainingFiles">The list of remaining filenames.</param>
- /// <param name="baseName">The base name to use for the comparison.</param>
- /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
- /// <returns>A list of video extras for [baseName].</returns>
- private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
- {
- return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
- }
-
- /// <summary>
- /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
- /// </summary>
- /// <param name="remainingFiles">The list of remaining filenames.</param>
- /// <param name="firstBaseName">The first base name to use for the comparison.</param>
- /// <param name="secondBaseName">The second base name to use for the comparison.</param>
- /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
- /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
- private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
- {
- var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
- var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
-
- var result = new List<VideoFileInfo>();
- for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
- {
- var file = remainingFiles[pos];
- if (file.ExtraType == null)
- {
- continue;
- }
-
- var filename = file.FileNameWithoutExtension;
- if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
- || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
- {
- result.Add(file);
- remainingFiles.RemoveAt(pos);
- }
- }
-
- return result;
- }
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 3b1d906c6..de8e177d8 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -16,10 +16,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
+ /// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>VideoFileInfo.</returns>
- public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
{
- return Resolve(path, true, namingOptions);
+ return Resolve(path, true, namingOptions, parseName);
}
/// <summary>
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
+ var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
var name = Path.GetFileNameWithoutExtension(path);
@@ -87,9 +88,9 @@ namespace Emby.Naming.Video
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
- && TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
+ && TryCleanString(name, namingOptions, out var newName))
{
- name = newName.ToString();
+ name = newName;
}
}
@@ -138,7 +139,7 @@ namespace Emby.Naming.Video
/// <param name="namingOptions">The naming options.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
- public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
+ public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out string newName)
{
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}