Merge pull request #4456 from martinek-stepan/emby-namig-nullable

Emby.Naming - nullable & code coverage
This commit is contained in:
Joshua M. Boniface 2020-11-17 10:28:39 -05:00 committed by GitHub
commit f6ebdbc45e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1755 additions and 809 deletions

View File

@ -1,6 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
@ -9,15 +6,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.Audio
{
/// <summary>
/// Helper class to determine if Album is multipart.
/// </summary>
public class AlbumParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="AlbumParser"/> class.
/// </summary>
/// <param name="options">Naming options containing AlbumStackingPrefixes.</param>
public AlbumParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Function that determines if album is multipart.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>True if album is multipart.</returns>
public bool IsMultiPart(string path)
{
var filename = Path.GetFileName(path);

View File

@ -1,6 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
@ -8,8 +5,17 @@ using Emby.Naming.Common;
namespace Emby.Naming.Audio
{
/// <summary>
/// Static helper class to determine if file at path is audio file.
/// </summary>
public static class AudioFileParser
{
/// <summary>
/// Static helper method to determine if file at path is audio file.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="options"><see cref="NamingOptions"/> containing AudioFileExtensions.</param>
/// <returns>True if file at path is audio file.</returns>
public static bool IsAudioFile(string path, NamingOptions options)
{
var extension = Path.GetExtension(path);

View File

@ -7,6 +7,21 @@ namespace Emby.Naming.AudioBook
/// </summary>
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>
@ -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))
{

View File

@ -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;
}
}

View File

@ -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; }
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -1,28 +1,32 @@
#pragma warning disable CS1591
using System;
using System.Text.RegularExpressions;
namespace Emby.Naming.Common
{
/// <summary>
/// Regular expressions for parsing TV Episodes.
/// </summary>
public class EpisodeExpression
{
private string _expression;
private Regex _regex;
private Regex? _regex;
public EpisodeExpression(string expression, bool byDate)
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeExpression"/> class.
/// </summary>
/// <param name="expression">Regular expressions.</param>
/// <param name="byDate">True if date is expected.</param>
public EpisodeExpression(string expression, bool byDate = false)
{
Expression = expression;
_expression = expression;
IsByDate = byDate;
DateTimeFormats = Array.Empty<string>();
SupportsAbsoluteEpisodeNumbers = true;
}
public EpisodeExpression(string expression)
: this(expression, false)
{
}
/// <summary>
/// Gets or sets raw expressions string.
/// </summary>
public string Expression
{
get => _expression;
@ -33,16 +37,34 @@ namespace Emby.Naming.Common
}
}
/// <summary>
/// Gets or sets a value indicating whether gets or sets property indicating if date can be find in expression.
/// </summary>
public bool IsByDate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets property indicating if expression is optimistic.
/// </summary>
public bool IsOptimistic { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets property indicating if expression is named.
/// </summary>
public bool IsNamed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets property indicating if expression supports episodes with absolute numbers.
/// </summary>
public bool SupportsAbsoluteEpisodeNumbers { get; set; }
/// <summary>
/// Gets or sets optional list of date formats used for date parsing.
/// </summary>
public string[] DateTimeFormats { get; set; }
/// <summary>
/// Gets a <see cref="Regex"/> expressions objects (creates it if null).
/// </summary>
public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
}

View File

@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace Emby.Naming.Common
{
/// <summary>
/// Type of audiovisual media.
/// </summary>
public enum MediaType
{
/// <summary>

View File

@ -1,15 +1,21 @@
#pragma warning disable CS1591
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
// ReSharper disable StringLiteralTypo
namespace Emby.Naming.Common
{
/// <summary>
/// Big ugly class containing lot of different naming options that should be split and injected instead of passes everywhere.
/// </summary>
public class NamingOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="NamingOptions"/> class.
/// </summary>
public NamingOptions()
{
VideoFileExtensions = new[]
@ -75,63 +81,52 @@ namespace Emby.Naming.Common
StubTypes = new[]
{
new StubTypeRule
{
StubType = "dvd",
Token = "dvd"
},
new StubTypeRule
{
StubType = "hddvd",
Token = "hddvd"
},
new StubTypeRule
{
StubType = "bluray",
Token = "bluray"
},
new StubTypeRule
{
StubType = "bluray",
Token = "brrip"
},
new StubTypeRule
{
StubType = "bluray",
Token = "bd25"
},
new StubTypeRule
{
StubType = "bluray",
Token = "bd50"
},
new StubTypeRule
{
StubType = "vhs",
Token = "vhs"
},
new StubTypeRule
{
StubType = "tv",
Token = "HDTV"
},
new StubTypeRule
{
StubType = "tv",
Token = "PDTV"
},
new StubTypeRule
{
StubType = "tv",
Token = "DSR"
}
new StubTypeRule(
stubType: "dvd",
token: "dvd"),
new StubTypeRule(
stubType: "hddvd",
token: "hddvd"),
new StubTypeRule(
stubType: "bluray",
token: "bluray"),
new StubTypeRule(
stubType: "bluray",
token: "brrip"),
new StubTypeRule(
stubType: "bluray",
token: "bd25"),
new StubTypeRule(
stubType: "bluray",
token: "bd50"),
new StubTypeRule(
stubType: "vhs",
token: "vhs"),
new StubTypeRule(
stubType: "tv",
token: "HDTV"),
new StubTypeRule(
stubType: "tv",
token: "PDTV"),
new StubTypeRule(
stubType: "tv",
token: "DSR")
};
VideoFileStackingExpressions = new[]
{
"(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$",
"(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(.*?)(\\.[^.]+)$",
"(.*?)([ ._-]*[a-d])(.*?)(\\.[^.]+)$"
"(?<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>\\.[^.]+)$"
};
CleanDateTimes = new[]
@ -142,7 +137,7 @@ 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|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"[ _\,\.\(\)\[\]\-](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|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])"
};
@ -255,7 +250,7 @@ namespace Emby.Naming.Common
},
// <!-- foo.ep01, foo.EP_01 -->
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true)
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
{
@ -264,7 +259,7 @@ namespace Emby.Naming.Common
"yyyy_MM_dd"
}
},
new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true)
new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[0-9]{4})", true)
{
DateTimeFormats = new[]
{
@ -286,7 +281,12 @@ namespace Emby.Naming.Common
{
SupportsAbsoluteEpisodeNumbers = true
},
new EpisodeExpression(@"[\\\\/\\._ -](?<seriesname>(?![0-9]+[0-9][0-9])([^\\\/])*)[\\\\/\\._ -](?<seasonnumber>[0-9]+)(?<epnumber>[0-9][0-9](?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([\\._ -][^\\\\/]*)$")
// Case Closed (1996-2007)/Case Closed - 317.mkv
// /server/anything_102.mp4
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
// /server/anything_1996.11.14.mp4
new EpisodeExpression(@"[\\/._ -](?<seriesname>(?![0-9]+[0-9][0-9])([^\\\/_])*)[\\\/._ -](?<seasonnumber>[0-9]+)(?<epnumber>[0-9][0-9](?:(?:[a-i]|\.[1-9])(?![0-9]))?)([._ -][^\\\/]*)$")
{
IsOptimistic = true,
IsNamed = true,
@ -381,247 +381,193 @@ namespace Emby.Naming.Common
VideoExtraRules = new[]
{
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Filename,
Token = "trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = "-trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = ".trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = "_trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = " trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Filename,
Token = "sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = "-sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = ".sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = "_sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = " sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.ThemeSong,
RuleType = ExtraRuleType.Filename,
Token = "theme",
MediaType = MediaType.Audio
},
new ExtraRule
{
ExtraType = ExtraType.Scene,
RuleType = ExtraRuleType.Suffix,
Token = "-scene",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-clip",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Interview,
RuleType = ExtraRuleType.Suffix,
Token = "-interview",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.BehindTheScenes,
RuleType = ExtraRuleType.Suffix,
Token = "-behindthescenes",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.DeletedScene,
RuleType = ExtraRuleType.Suffix,
Token = "-deleted",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-featurette",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-short",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.BehindTheScenes,
RuleType = ExtraRuleType.DirectoryName,
Token = "behind the scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.DeletedScene,
RuleType = ExtraRuleType.DirectoryName,
Token = "deleted scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Interview,
RuleType = ExtraRuleType.DirectoryName,
Token = "interviews",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Scene,
RuleType = ExtraRuleType.DirectoryName,
Token = "scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.DirectoryName,
Token = "samples",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.DirectoryName,
Token = "shorts",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.DirectoryName,
Token = "featurettes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Unknown,
RuleType = ExtraRuleType.DirectoryName,
Token = "extras",
MediaType = MediaType.Video,
},
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Filename,
"trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
"-trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
".trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
"_trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
" trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Filename,
"sample",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
"-sample",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
".sample",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
"_sample",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
" sample",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
"-scene",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-clip",
MediaType.Video),
new ExtraRule(
ExtraType.Interview,
ExtraRuleType.Suffix,
"-interview",
MediaType.Video),
new ExtraRule(
ExtraType.BehindTheScenes,
ExtraRuleType.Suffix,
"-behindthescenes",
MediaType.Video),
new ExtraRule(
ExtraType.DeletedScene,
ExtraRuleType.Suffix,
"-deleted",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-featurette",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-short",
MediaType.Video),
new ExtraRule(
ExtraType.BehindTheScenes,
ExtraRuleType.DirectoryName,
"behind the scenes",
MediaType.Video),
new ExtraRule(
ExtraType.DeletedScene,
ExtraRuleType.DirectoryName,
"deleted scenes",
MediaType.Video),
new ExtraRule(
ExtraType.Interview,
ExtraRuleType.DirectoryName,
"interviews",
MediaType.Video),
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.DirectoryName,
"scenes",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.DirectoryName,
"samples",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.DirectoryName,
"shorts",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.DirectoryName,
"featurettes",
MediaType.Video),
new ExtraRule(
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extras",
MediaType.Video),
};
Format3DRules = new[]
{
// Kodi rules:
new Format3DRule
{
PreceedingToken = "3d",
Token = "hsbs"
},
new Format3DRule
{
PreceedingToken = "3d",
Token = "sbs"
},
new Format3DRule
{
PreceedingToken = "3d",
Token = "htab"
},
new Format3DRule
{
PreceedingToken = "3d",
Token = "tab"
},
// Media Browser rules:
new Format3DRule
{
Token = "fsbs"
},
new Format3DRule
{
Token = "hsbs"
},
new Format3DRule
{
Token = "sbs"
},
new Format3DRule
{
Token = "ftab"
},
new Format3DRule
{
Token = "htab"
},
new Format3DRule
{
Token = "tab"
},
new Format3DRule
{
Token = "sbs3d"
},
new Format3DRule
{
Token = "mvc"
}
new Format3DRule(
precedingToken: "3d",
token: "hsbs"),
new Format3DRule(
precedingToken: "3d",
token: "sbs"),
new Format3DRule(
precedingToken: "3d",
token: "htab"),
new Format3DRule(
precedingToken: "3d",
token: "tab"),
// Media Browser rules:
new Format3DRule("fsbs"),
new Format3DRule("hsbs"),
new Format3DRule("sbs"),
new Format3DRule("ftab"),
new Format3DRule("htab"),
new Format3DRule("tab"),
new Format3DRule("sbs3d"),
new Format3DRule("mvc")
};
AudioBookPartsExpressions = new[]
{
// Detect specified chapters, like CH 01
@ -631,13 +577,20 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?<chapter>[0-9]+)",
// Part if often ending of filename
"(?<part>[0-9]+)$",
@"(?<!ch(?:apter) )(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
};
AudioBookNamesExpressions = new[]
{
// Detect year usually in brackets after name Batman (2020)
@"^(?<name>.+?)\s*\(\s*(?<year>\d{4})\s*\)\s*$",
@"^\s*(?<name>[^ ].*?)\s*$"
};
var extensions = VideoFileExtensions.ToList();
extensions.AddRange(new[]
@ -673,7 +626,7 @@ namespace Emby.Naming.Common
".mxf"
});
MultipleEpisodeExpressions = new string[]
MultipleEpisodeExpressions = new[]
{
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@ -697,56 +650,139 @@ namespace Emby.Naming.Common
Compile();
}
/// <summary>
/// Gets or sets list of audio file extensions.
/// </summary>
public string[] AudioFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of album stacking prefixes.
/// </summary>
public string[] AlbumStackingPrefixes { get; set; }
/// <summary>
/// Gets or sets list of subtitle file extensions.
/// </summary>
public string[] SubtitleFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of subtitles flag delimiters.
/// </summary>
public char[] SubtitleFlagDelimiters { get; set; }
/// <summary>
/// Gets or sets list of subtitle forced flags.
/// </summary>
public string[] SubtitleForcedFlags { get; set; }
/// <summary>
/// Gets or sets list of subtitle default flags.
/// </summary>
public string[] SubtitleDefaultFlags { get; set; }
/// <summary>
/// Gets or sets list of episode regular expressions.
/// </summary>
public EpisodeExpression[] EpisodeExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw episode without season regular expressions strings.
/// </summary>
public string[] EpisodeWithoutSeasonExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw multi-part episodes regular expressions strings.
/// </summary>
public string[] EpisodeMultiPartExpressions { get; set; }
/// <summary>
/// Gets or sets list of video file extensions.
/// </summary>
public string[] VideoFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of video stub file extensions.
/// </summary>
public string[] StubFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of raw audiobook parts regular expressions strings.
/// </summary>
public string[] AudioBookPartsExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw audiobook names regular expressions strings.
/// </summary>
public string[] AudioBookNamesExpressions { get; set; }
/// <summary>
/// Gets or sets list of stub type rules.
/// </summary>
public StubTypeRule[] StubTypes { get; set; }
/// <summary>
/// Gets or sets list of video flag delimiters.
/// </summary>
public char[] VideoFlagDelimiters { get; set; }
/// <summary>
/// Gets or sets list of 3D Format rules.
/// </summary>
public Format3DRule[] Format3DRules { get; set; }
/// <summary>
/// Gets or sets list of raw video file-stacking expressions strings.
/// </summary>
public string[] VideoFileStackingExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw clean DateTimes regular expressions strings.
/// </summary>
public string[] CleanDateTimes { get; set; }
/// <summary>
/// Gets or sets list of raw clean strings regular expressions strings.
/// </summary>
public string[] CleanStrings { get; set; }
/// <summary>
/// Gets or sets list of multi-episode regular expressions.
/// </summary>
public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
/// <summary>
/// Gets or sets list of extra rules for videos.
/// </summary>
public ExtraRule[] VideoExtraRules { get; set; }
public Regex[] VideoFileStackingRegexes { get; private set; }
/// <summary>
/// Gets list of video file-stack regular expressions.
/// </summary>
public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] CleanDateTimeRegexes { get; private set; }
/// <summary>
/// Gets list of clean datetime regular expressions.
/// </summary>
public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] CleanStringRegexes { get; private set; }
/// <summary>
/// Gets list of clean string regular expressions.
/// </summary>
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] EpisodeWithoutSeasonRegexes { get; private set; }
/// <summary>
/// Gets list of episode without season regular expressions.
/// </summary>
public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] EpisodeMultiPartRegexes { get; private set; }
/// <summary>
/// Gets list of multi-part episode regular expressions.
/// </summary>
public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Compiles raw regex strings into regexes.
/// </summary>
public void Compile()
{
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();

View File

@ -14,6 +14,7 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@ -38,7 +39,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->

View File

@ -1,9 +1,23 @@
#pragma warning disable CS1591
namespace Emby.Naming.Subtitles
{
/// <summary>
/// Class holding information about subtitle.
/// </summary>
public class SubtitleInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleInfo"/> class.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="isDefault">Is subtitle default.</param>
/// <param name="isForced">Is subtitle forced.</param>
public SubtitleInfo(string path, bool isDefault, bool isForced)
{
Path = path;
IsDefault = isDefault;
IsForced = isForced;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
@ -14,7 +28,7 @@ namespace Emby.Naming.Subtitles
/// Gets or sets the language.
/// </summary>
/// <value>The language.</value>
public string Language { get; set; }
public string? Language { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is default.

View File

@ -1,6 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
@ -8,20 +5,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.Subtitles
{
/// <summary>
/// Subtitle Parser class.
/// </summary>
public class SubtitleParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param>
public SubtitleParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns>
public SubtitleInfo? ParseFile(string path)
{
if (path.Length == 0)
{
throw new ArgumentException("File path can't be empty.", nameof(path));
return null;
}
var extension = Path.GetExtension(path);
@ -31,12 +40,10 @@ namespace Emby.Naming.Subtitles
}
var flags = GetFlags(path);
var info = new SubtitleInfo
{
Path = path,
IsDefault = _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
IsForced = _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase))
};
var info = new SubtitleInfo(
path,
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))

View File

@ -1,9 +1,19 @@
#pragma warning disable CS1591
namespace Emby.Naming.TV
{
/// <summary>
/// Holder object for Episode information.
/// </summary>
public class EpisodeInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeInfo"/> class.
/// </summary>
/// <param name="path">Path to the file.</param>
public EpisodeInfo(string path)
{
Path = path;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
@ -14,19 +24,19 @@ namespace Emby.Naming.TV
/// Gets or sets the container.
/// </summary>
/// <value>The container.</value>
public string Container { get; set; }
public string? Container { get; set; }
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
public string SeriesName { get; set; }
public string? SeriesName { get; set; }
/// <summary>
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
public string Format3D { get; set; }
public string? Format3D { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [is3 d].
@ -44,20 +54,41 @@ namespace Emby.Naming.TV
/// Gets or sets the type of the stub.
/// </summary>
/// <value>The type of the stub.</value>
public string StubType { get; set; }
public string? StubType { get; set; }
/// <summary>
/// Gets or sets optional season number.
/// </summary>
public int? SeasonNumber { get; set; }
/// <summary>
/// Gets or sets optional episode number.
/// </summary>
public int? EpisodeNumber { get; set; }
public int? EndingEpsiodeNumber { get; set; }
/// <summary>
/// Gets or sets optional ending episode number. For multi-episode files 1-13.
/// </summary>
public int? EndingEpisodeNumber { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Month { get; set; }
/// <summary>
/// Gets or sets optional day of release.
/// </summary>
public int? Day { get; set; }
/// <summary>
/// Gets or sets a value indicating whether by date expression was used.
/// </summary>
public bool IsByDate { get; set; }
}
}

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
@ -9,15 +6,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to parse information about episode from path.
/// </summary>
public class EpisodePathParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="EpisodePathParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
public EpisodePathParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Parses information about episode from path.
/// </summary>
/// <param name="path">Path.</param>
/// <param name="isDirectory">Is path for a directory or file.</param>
/// <param name="isNamed">Do we want to use IsNamed expressions.</param>
/// <param name="isOptimistic">Do we want to use Optimistic expressions.</param>
/// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</param>
/// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param>
/// <returns>Returns <see cref="EpisodePathParserResult"/> object.</returns>
public EpisodePathParserResult Parse(
string path,
bool isDirectory,
@ -146,7 +160,7 @@ namespace Emby.Naming.TV
{
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EndingEpsiodeNumber = num;
result.EndingEpisodeNumber = num;
}
}
}
@ -186,7 +200,7 @@ namespace Emby.Naming.TV
private void FillAdditional(string path, EpisodePathParserResult info)
{
var expressions = _options.MultipleEpisodeExpressions.ToList();
var expressions = _options.MultipleEpisodeExpressions.Where(i => i.IsNamed).ToList();
if (string.IsNullOrEmpty(info.SeriesName))
{
@ -200,11 +214,6 @@ namespace Emby.Naming.TV
{
foreach (var i in expressions)
{
if (!i.IsNamed)
{
continue;
}
var result = Parse(path, i);
if (!result.Success)
@ -217,13 +226,13 @@ namespace Emby.Naming.TV
info.SeriesName = result.SeriesName;
}
if (!info.EndingEpsiodeNumber.HasValue && info.EpisodeNumber.HasValue)
if (!info.EndingEpisodeNumber.HasValue && info.EpisodeNumber.HasValue)
{
info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
info.EndingEpisodeNumber = result.EndingEpisodeNumber;
}
if (!string.IsNullOrEmpty(info.SeriesName)
&& (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
&& (!info.EpisodeNumber.HasValue || info.EndingEpisodeNumber.HasValue))
{
break;
}

View File

@ -1,25 +1,54 @@
#pragma warning disable CS1591
namespace Emby.Naming.TV
{
/// <summary>
/// Holder object for <see cref="EpisodePathParser"/> result.
/// </summary>
public class EpisodePathParserResult
{
/// <summary>
/// Gets or sets optional season number.
/// </summary>
public int? SeasonNumber { get; set; }
/// <summary>
/// Gets or sets optional episode number.
/// </summary>
public int? EpisodeNumber { get; set; }
public int? EndingEpsiodeNumber { get; set; }
/// <summary>
/// Gets or sets optional ending episode number. For multi-episode files 1-13.
/// </summary>
public int? EndingEpisodeNumber { get; set; }
public string SeriesName { get; set; }
/// <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; }
/// <summary>
/// Gets or sets a value indicating whether by date expression was used.
/// </summary>
public bool IsByDate { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Month { get; set; }
/// <summary>
/// Gets or sets optional day of release.
/// </summary>
public int? Day { get; set; }
}
}

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.IO;
using System.Linq;
@ -9,15 +6,32 @@ using Emby.Naming.Video;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to resolve information about episode from path.
/// </summary>
public class EpisodeResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
public EpisodeResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolve information about episode from path.
/// </summary>
/// <param name="path">Path.</param>
/// <param name="isDirectory">Is path for a directory or file.</param>
/// <param name="isNamed">Do we want to use IsNamed expressions.</param>
/// <param name="isOptimistic">Do we want to use Optimistic expressions.</param>
/// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</param>
/// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param>
/// <returns>Returns null or <see cref="EpisodeInfo"/> object if successful.</returns>
public EpisodeInfo? Resolve(
string path,
bool isDirectory,
@ -54,12 +68,11 @@ namespace Emby.Naming.TV
var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
return new EpisodeInfo
return new EpisodeInfo(path)
{
Path = path,
Container = container,
IsStub = isStub,
EndingEpsiodeNumber = parsingResult.EndingEpsiodeNumber,
EndingEpisodeNumber = parsingResult.EndingEpisodeNumber,
EpisodeNumber = parsingResult.EpisodeNumber,
SeasonNumber = parsingResult.SeasonNumber,
SeriesName = parsingResult.SeriesName,

View File

@ -1,11 +1,12 @@
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
namespace Emby.Naming.TV
{
/// <summary>
/// Class to parse season paths.
/// </summary>
public static class SeasonPathParser
{
/// <summary>
@ -23,6 +24,13 @@ namespace Emby.Naming.TV
"stagione"
};
/// <summary>
/// Attempts to parse season number from path.
/// </summary>
/// <param name="path">Path to season.</param>
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{
var result = new SeasonPathParserResult();
@ -101,9 +109,9 @@ namespace Emby.Naming.TV
}
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < parts.Length; i++)
foreach (var part in parts)
{
if (TryGetSeasonNumberFromPart(parts[i], out int seasonNumber))
if (TryGetSeasonNumberFromPart(part, out int seasonNumber))
{
return (seasonNumber, true);
}
@ -139,7 +147,7 @@ namespace Emby.Naming.TV
var numericStart = -1;
var length = 0;
var hasOpenParenth = false;
var hasOpenParenthesis = false;
var isSeasonFolder = true;
// Find out where the numbers start, and then keep going until they end
@ -147,7 +155,7 @@ namespace Emby.Naming.TV
{
if (char.IsNumber(path[i]))
{
if (!hasOpenParenth)
if (!hasOpenParenthesis)
{
if (numericStart == -1)
{
@ -167,11 +175,11 @@ namespace Emby.Naming.TV
var currentChar = path[i];
if (currentChar == '(')
{
hasOpenParenth = true;
hasOpenParenthesis = true;
}
else if (currentChar == ')')
{
hasOpenParenth = false;
hasOpenParenthesis = false;
}
}

View File

@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace Emby.Naming.TV
{
/// <summary>
/// Data object to pass result of <see cref="SeasonPathParser"/>.
/// </summary>
public class SeasonPathParserResult
{
/// <summary>
@ -16,6 +17,10 @@ namespace Emby.Naming.TV
/// <value><c>true</c> if success; otherwise, <c>false</c>.</value>
public bool Success { get; set; }
/// <summary>
/// Gets or sets a value indicating whether "Is season folder".
/// Seems redundant and barely used.
/// </summary>
public bool IsSeasonFolder { get; set; }
}
}

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
@ -12,6 +9,12 @@ namespace Emby.Naming.Video
/// </summary>
public static class CleanDateTimeParser
{
/// <summary>
/// Attempts to clean the name.
/// </summary>
/// <param name="name">Name of video.</param>
/// <param name="cleanDateTimeRegexes">Optional list of regexes to clean the name.</param>
/// <returns>Returns <see cref="CleanDateTimeResult"/> object.</returns>
public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
{
CleanDateTimeResult result = new CleanDateTimeResult(name);

View File

@ -1,22 +1,21 @@
#pragma warning disable CS1591
#nullable enable
namespace Emby.Naming.Video
{
/// <summary>
/// Holder structure for name and year.
/// </summary>
public readonly struct CleanDateTimeResult
{
public CleanDateTimeResult(string name, int? year)
/// <summary>
/// Initializes a new instance of the <see cref="CleanDateTimeResult"/> struct.
/// </summary>
/// <param name="name">Name of video.</param>
/// <param name="year">Year of release.</param>
public CleanDateTimeResult(string name, int? year = null)
{
Name = name;
Year = year;
}
public CleanDateTimeResult(string name)
{
Name = name;
Year = null;
}
/// <summary>
/// Gets the name.
/// </summary>

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@ -12,6 +9,13 @@ namespace Emby.Naming.Video
/// </summary>
public static class CleanStringParser
{
/// <summary>
/// Attempts to extract clean name with regular expressions.
/// </summary>
/// <param name="name">Name of file.</param>
/// <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(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{
var len = expressions.Count;

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
@ -9,15 +7,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolve if file is extra for video.
/// </summary>
public class ExtraResolver
{
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;
}
/// <summary>
/// Attempts to resolve if file is extra.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public ExtraResult GetExtraInfo(string path)
{
return _options.VideoExtraRules
@ -43,10 +53,6 @@ namespace Emby.Naming.Video
return result;
}
}
else
{
return result;
}
if (rule.RuleType == ExtraRuleType.Filename)
{

View File

@ -1,9 +1,10 @@
#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
/// <summary>
/// Holder object for passing results from ExtraResolver.
/// </summary>
public class ExtraResult
{
/// <summary>
@ -16,6 +17,6 @@ namespace Emby.Naming.Video
/// Gets or sets the rule.
/// </summary>
/// <value>The rule.</value>
public ExtraRule Rule { get; set; }
public ExtraRule? Rule { get; set; }
}
}

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
using MediaType = Emby.Naming.Common.MediaType;
@ -10,6 +8,21 @@ namespace Emby.Naming.Video
/// </summary>
public class ExtraRule
{
/// <summary>
/// Initializes a new instance of the <see cref="ExtraRule"/> class.
/// </summary>
/// <param name="extraType">Type of extra.</param>
/// <param name="ruleType">Type of rule.</param>
/// <param name="token">Token.</param>
/// <param name="mediaType">Media type.</param>
public ExtraRule(ExtraType extraType, ExtraRuleType ruleType, string token, MediaType mediaType)
{
Token = token;
ExtraType = extraType;
RuleType = ruleType;
MediaType = mediaType;
}
/// <summary>
/// Gets or sets the token to use for matching against the file path.
/// </summary>

View File

@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace Emby.Naming.Video
{
/// <summary>
/// Extra rules type to determine against what <see cref="ExtraRule.Token"/> should be matched.
/// </summary>
public enum ExtraRuleType
{
/// <summary>
@ -22,6 +23,6 @@ namespace Emby.Naming.Video
/// <summary>
/// Match <see cref="ExtraRule.Token"/> against the name of the directory containing the file.
/// </summary>
DirectoryName = 3,
DirectoryName = 3
}
}

View File

@ -1,24 +1,43 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
namespace Emby.Naming.Video
{
/// <summary>
/// Object holding list of files paths with additional information.
/// </summary>
public class FileStack
{
/// <summary>
/// Initializes a new instance of the <see cref="FileStack"/> class.
/// </summary>
public FileStack()
{
Files = new List<string>();
}
public string Name { get; set; }
/// <summary>
/// Gets or sets name of file stack.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets list of paths in stack.
/// </summary>
public List<string> Files { get; set; }
/// <summary>
/// Gets or sets a value indicating whether stack is directory stack.
/// </summary>
public bool IsDirectoryStack { get; set; }
/// <summary>
/// Helper function to determine if path is in the stack.
/// </summary>
/// <param name="file">Path of desired file.</param>
/// <param name="isDirectory">Requested type of stack.</param>
/// <returns>True if file is in the stack.</returns>
public bool ContainsFile(string file, bool isDirectory)
{
if (IsDirectoryStack == isDirectory)

View File

@ -1,37 +1,53 @@
#pragma warning disable CS1591
using System;
using System.IO;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Parses list of flags from filename based on delimiters.
/// </summary>
public class FlagParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="FlagParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
public FlagParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>List of found flags.</returns>
public string[] GetFlags(string path)
{
return GetFlags(path, _options.VideoFlagDelimiters);
}
public string[] GetFlags(string path, char[] delimeters)
/// <summary>
/// Parses flags from filename based on delimiters.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="delimiters">Delimiters used to extract flags.</param>
/// <returns>List of found flags.</returns>
public string[] GetFlags(string path, char[] delimiters)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
return Array.Empty<string>();
}
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path);
return file.Split(delimeters, StringSplitOptions.RemoveEmptyEntries);
return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
}
}
}

View File

@ -1,28 +1,38 @@
#pragma warning disable CS1591
using System;
using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Parste 3D format related flags.
/// </summary>
public class Format3DParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="Format3DParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
public Format3DParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Parse 3D format related flags.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
public Format3DResult Parse(string path)
{
int oldLen = _options.VideoFlagDelimiters.Length;
var delimeters = new char[oldLen + 1];
_options.VideoFlagDelimiters.CopyTo(delimeters, 0);
delimeters[oldLen] = ' ';
var delimiters = new char[oldLen + 1];
_options.VideoFlagDelimiters.CopyTo(delimiters, 0);
delimiters[oldLen] = ' ';
return Parse(new FlagParser(_options).GetFlags(path, delimeters));
return Parse(new FlagParser(_options).GetFlags(path, delimiters));
}
internal Format3DResult Parse(string[] videoFlags)
@ -44,7 +54,7 @@ namespace Emby.Naming.Video
{
var result = new Format3DResult();
if (string.IsNullOrEmpty(rule.PreceedingToken))
if (string.IsNullOrEmpty(rule.PrecedingToken))
{
result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
result.Is3D = !string.IsNullOrEmpty(result.Format3D);
@ -57,13 +67,13 @@ namespace Emby.Naming.Video
else
{
var foundPrefix = false;
string format = null;
string? format = null;
foreach (var flag in videoFlags)
{
if (foundPrefix)
{
result.Tokens.Add(rule.PreceedingToken);
result.Tokens.Add(rule.PrecedingToken);
if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
{
@ -74,7 +84,7 @@ namespace Emby.Naming.Video
break;
}
foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase);
foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
}
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);

View File

@ -1,11 +1,15 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace Emby.Naming.Video
{
/// <summary>
/// Helper object to return data from <see cref="Format3DParser"/>.
/// </summary>
public class Format3DResult
{
/// <summary>
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
/// </summary>
public Format3DResult()
{
Tokens = new List<string>();
@ -21,7 +25,7 @@ namespace Emby.Naming.Video
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
public string Format3D { get; set; }
public string? Format3D { get; set; }
/// <summary>
/// Gets or sets the tokens.

View File

@ -1,9 +1,21 @@
#pragma warning disable CS1591
namespace Emby.Naming.Video
{
/// <summary>
/// Data holder class for 3D format rule.
/// </summary>
public class Format3DRule
{
/// <summary>
/// Initializes a new instance of the <see cref="Format3DRule"/> class.
/// </summary>
/// <param name="token">Token.</param>
/// <param name="precedingToken">Token present before current token.</param>
public Format3DRule(string token, string? precedingToken = null)
{
Token = token;
PrecedingToken = precedingToken;
}
/// <summary>
/// Gets or sets the token.
/// </summary>
@ -11,9 +23,9 @@ namespace Emby.Naming.Video
public string Token { get; set; }
/// <summary>
/// Gets or sets the preceeding token.
/// Gets or sets the preceding token.
/// </summary>
/// <value>The preceeding token.</value>
public string PreceedingToken { get; set; }
/// <value>The preceding token.</value>
public string? PrecedingToken { get; set; }
}
}

View File

@ -1,58 +1,88 @@
#pragma warning disable CS1591
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;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolve <see cref="FileStack"/> from list of paths.
/// </summary>
public 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>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
}
/// <summary>
/// Resolves only files from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
}
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<FileSystemMetadata> files)
/// <summary>
/// Resolves audiobooks from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
{
var groupedDirectoryFiles = files.GroupBy(file =>
file.IsDirectory
? file.FullName
: Path.GetDirectoryName(file.FullName));
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
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))
{
if (file.IsDirectory)
foreach (var file in directory)
{
continue;
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);
}
stack.Files.Add(file.FullName);
yield return stack;
}
yield return stack;
}
}
/// <summary>
/// Resolves videos from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{
var resolver = new VideoResolver(_options);
@ -81,10 +111,10 @@ namespace Emby.Naming.Video
if (match1.Success)
{
var title1 = match1.Groups[1].Value;
var volume1 = match1.Groups[2].Value;
var ignore1 = match1.Groups[3].Value;
var extension1 = match1.Groups[4].Value;
var title1 = match1.Groups["title"].Value;
var volume1 = match1.Groups["volume"].Value;
var ignore1 = match1.Groups["ignore"].Value;
var extension1 = match1.Groups["extension"].Value;
var j = i + 1;
while (j < list.Count)

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.IO;
using System.Linq;
@ -8,13 +5,23 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolve if file is stub (.disc).
/// </summary>
public static class StubResolver
{
/// <summary>
/// Tries to resolve if file is stub (.disc).
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="options">NamingOptions containing StubFileExtensions and StubTypes.</param>
/// <param name="stubType">Stub type.</param>
/// <returns>True if file is a stub.</returns>
public static bool TryResolveFile(string path, NamingOptions options, out string? stubType)
{
stubType = default;
if (path == null)
if (string.IsNullOrEmpty(path))
{
return false;
}

View File

@ -1,19 +0,0 @@
#pragma warning disable CS1591
namespace Emby.Naming.Video
{
public struct StubResult
{
/// <summary>
/// Gets or sets a value indicating whether this instance is stub.
/// </summary>
/// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
public bool IsStub { get; set; }
/// <summary>
/// Gets or sets the type of the stub.
/// </summary>
/// <value>The type of the stub.</value>
public string StubType { get; set; }
}
}

View File

@ -1,9 +1,21 @@
#pragma warning disable CS1591
namespace Emby.Naming.Video
{
/// <summary>
/// Data class holding information about Stub type rule.
/// </summary>
public class StubTypeRule
{
/// <summary>
/// Initializes a new instance of the <see cref="StubTypeRule"/> class.
/// </summary>
/// <param name="token">Token.</param>
/// <param name="stubType">Stub type.</param>
public StubTypeRule(string token, string stubType)
{
Token = token;
StubType = stubType;
}
/// <summary>
/// Gets or sets the token.
/// </summary>

View File

@ -7,6 +7,35 @@ namespace Emby.Naming.Video
/// </summary>
public class VideoFileInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="VideoFileInfo"/> class.
/// </summary>
/// <param name="name">Name of file.</param>
/// <param name="path">Path to the file.</param>
/// <param name="container">Container type.</param>
/// <param name="year">Year of release.</param>
/// <param name="extraType">Extra type.</param>
/// <param name="extraRule">Extra rule.</param>
/// <param name="format3D">Format 3D.</param>
/// <param name="is3D">Is 3D.</param>
/// <param name="isStub">Is Stub.</param>
/// <param name="stubType">Stub type.</param>
/// <param name="isDirectory">Is directory.</param>
public VideoFileInfo(string name, string path, string? container, int? year = default, ExtraType? extraType = default, ExtraRule? extraRule = default, string? format3D = default, bool is3D = default, bool isStub = default, string? stubType = default, bool isDirectory = default)
{
Path = path;
Container = container;
Name = name;
Year = year;
ExtraType = extraType;
ExtraRule = extraRule;
Format3D = format3D;
Is3D = is3D;
IsStub = isStub;
StubType = stubType;
IsDirectory = isDirectory;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
@ -17,7 +46,7 @@ namespace Emby.Naming.Video
/// Gets or sets the container.
/// </summary>
/// <value>The container.</value>
public string Container { get; set; }
public string? Container { get; set; }
/// <summary>
/// Gets or sets the name.
@ -41,13 +70,13 @@ namespace Emby.Naming.Video
/// Gets or sets the extra rule.
/// </summary>
/// <value>The extra rule.</value>
public ExtraRule ExtraRule { get; set; }
public ExtraRule? ExtraRule { get; set; }
/// <summary>
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
public string Format3D { get; set; }
public string? Format3D { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [is3 d].
@ -65,7 +94,7 @@ namespace Emby.Naming.Video
/// Gets or sets the type of the stub.
/// </summary>
/// <value>The type of the stub.</value>
public string StubType { get; set; }
public string? StubType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is a directory.
@ -84,8 +113,7 @@ namespace Emby.Naming.Video
/// <inheritdoc />
public override string ToString()
{
// Makes debugging easier
return Name ?? base.ToString();
return "VideoFileInfo(Name: '" + Name + "')";
}
}
}

View File

@ -12,7 +12,7 @@ namespace Emby.Naming.Video
/// Initializes a new instance of the <see cref="VideoInfo" /> class.
/// </summary>
/// <param name="name">The name.</param>
public VideoInfo(string name)
public VideoInfo(string? name)
{
Name = name;
@ -25,7 +25,7 @@ namespace Emby.Naming.Video
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// Gets or sets the year.

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
@ -11,22 +9,35 @@ using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
public class VideoListResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
public VideoListResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="files">List of related video files.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files togeather when related.</returns>
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
{
var videoResolver = new VideoResolver(_options);
var videoInfos = files
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
.Where(i => i != null)
.OfType<VideoFileInfo>()
.ToList();
// Filter out all extras, otherwise they could cause stacks to not be resolved
@ -39,7 +50,7 @@ namespace Emby.Naming.Video
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos
.Where(i => !stackResult.Any(s => s.ContainsFile(i.Path, i.IsDirectory)))
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
.ToList();
var list = new List<VideoInfo>();
@ -48,7 +59,9 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList()
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
@ -133,7 +146,7 @@ namespace Emby.Naming.Video
}
// If there's only one video, accept all trailers
// Be lenient because people use all kinds of mish mash conventions with trailers
// Be lenient because people use all kinds of mishmash conventions with trailers.
if (list.Count == 1)
{
var trailers = remainingFiles
@ -203,15 +216,21 @@ namespace Emby.Naming.Video
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
}
private bool IsEligibleForMultiVersion(string folderName, string testFilename)
private bool IsEligibleForMultiVersion(string folderName, string? testFilename)
{
testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.ToString();
}
testFilename = testFilename.Substring(folderName.Length).Trim();
return string.IsNullOrEmpty(testFilename)
|| testFilename[0] == '-'
|| testFilename[0].Equals('-')
|| testFilename[0].Equals('_')
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
}

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.IO;
using System.Linq;
@ -8,10 +5,18 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolves <see cref="VideoFileInfo"/> from file path.
/// </summary>
public class VideoResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="VideoResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
/// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
public VideoResolver(NamingOptions options)
{
_options = options;
@ -22,7 +27,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoFileInfo.</returns>
public VideoFileInfo? ResolveDirectory(string path)
public VideoFileInfo? ResolveDirectory(string? path)
{
return Resolve(path, true);
}
@ -32,7 +37,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoFileInfo.</returns>
public VideoFileInfo? ResolveFile(string path)
public VideoFileInfo? ResolveFile(string? path)
{
return Resolve(path, false);
}
@ -45,11 +50,11 @@ namespace Emby.Naming.Video
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
/// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
public VideoFileInfo? Resolve(string path, bool isDirectory, bool parseName = true)
public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
return null;
}
bool isStub = false;
@ -99,39 +104,58 @@ namespace Emby.Naming.Video
}
}
return new VideoFileInfo
{
Path = path,
Container = container,
IsStub = isStub,
Name = name,
Year = year,
StubType = stubType,
Is3D = format3DResult.Is3D,
Format3D = format3DResult.Format3D,
ExtraType = extraResult.ExtraType,
IsDirectory = isDirectory,
ExtraRule = extraResult.Rule
};
return new VideoFileInfo(
path: path,
container: container,
isStub: isStub,
name: name,
year: year,
stubType: stubType,
is3D: format3DResult.Is3D,
format3D: format3DResult.Format3D,
extraType: extraResult.ExtraType,
isDirectory: isDirectory,
extraRule: extraResult.Rule);
}
/// <summary>
/// Determines if path is video file based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>True if is video file.</returns>
public bool IsVideoFile(string path)
{
var extension = Path.GetExtension(path) ?? string.Empty;
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if path is video file stub based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>True if is video file stub.</returns>
public bool IsStubFile(string path)
{
var extension = Path.GetExtension(path) ?? string.Empty;
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Tries to clean name of clutter.
/// </summary>
/// <param name="name">Raw name.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
{
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
}
/// <summary>
/// Tries to get name and year from raw name.
/// </summary>
/// <param name="name">Raw name.</param>
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
public CleanDateTimeResult CleanDateTime(string name)
{
return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);

View File

@ -2486,9 +2486,10 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
// TODO nullable - what are we trying to do there with empty episodeInfo?
var episodeInfo = episode.IsFileProtocol
? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
: new Naming.TV.EpisodeInfo();
? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path)
: new Naming.TV.EpisodeInfo(episode.Path);
try
{
@ -2577,12 +2578,12 @@ namespace Emby.Server.Implementations.Library
if (!episode.IndexNumberEnd.HasValue || forceRefresh)
{
if (episode.IndexNumberEnd != episodeInfo.EndingEpsiodeNumber)
if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
{
changed = true;
}
episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber;
episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
}
if (!episode.ParentIndexNumber.HasValue || forceRefresh)

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>

View File

@ -1,4 +1,4 @@
using Emby.Naming.AudioBook;
using Emby.Naming.AudioBook;
using Xunit;
namespace Jellyfin.Naming.Tests.AudioBook
@ -8,22 +8,22 @@ namespace Jellyfin.Naming.Tests.AudioBook
[Fact]
public void CompareTo_Same_Success()
{
var info = new AudioBookFileInfo();
var info = new AudioBookFileInfo(string.Empty, string.Empty);
Assert.Equal(0, info.CompareTo(info));
}
[Fact]
public void CompareTo_Null_Success()
{
var info = new AudioBookFileInfo();
var info = new AudioBookFileInfo(string.Empty, string.Empty);
Assert.Equal(1, info.CompareTo(null));
}
[Fact]
public void CompareTo_Empty_Success()
{
var info1 = new AudioBookFileInfo();
var info2 = new AudioBookFileInfo();
var info1 = new AudioBookFileInfo(string.Empty, string.Empty);
var info2 = new AudioBookFileInfo(string.Empty, string.Empty);
Assert.Equal(0, info1.CompareTo(info2));
}
}

View File

@ -1,4 +1,6 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
@ -18,11 +20,22 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
"Harry Potter and the Deathly Hallows/Part 1.mp3",
"Harry Potter and the Deathly Hallows/Part 2.mp3",
"Harry Potter and the Deathly Hallows/book.nfo",
"Harry Potter and the Deathly Hallows/Extra.mp3",
"Batman/Chapter 1.mp3",
"Batman/Chapter 2.mp3",
"Batman/Chapter 3.mp3",
"Badman/audiobook.mp3",
"Badman/extra.mp3",
"Superman (2020)/Part 1.mp3",
"Superman (2020)/extra.mp3",
"Ready Player One (2020)/audiobook.mp3",
"Ready Player One (2020)/extra.mp3",
".mp3"
};
var resolver = GetResolver();
@ -33,13 +46,141 @@ namespace Jellyfin.Naming.Tests.AudioBook
FullName = i
})).ToList();
Assert.Equal(5, result.Count);
Assert.Equal(2, result[0].Files.Count);
// Assert.Empty(result[0].Extras); FIXME: AudioBookListResolver should resolve extra files properly
Assert.Single(result[0].Extras);
Assert.Equal("Harry Potter and the Deathly Hallows", result[0].Name);
Assert.Equal(3, result[1].Files.Count);
Assert.Empty(result[1].Extras);
Assert.Equal("Batman", result[1].Name);
Assert.Single(result[2].Files);
Assert.Single(result[2].Extras);
Assert.Equal("Badman", result[2].Name);
Assert.Single(result[3].Files);
Assert.Single(result[3].Extras);
Assert.Equal("Superman", result[3].Name);
Assert.Single(result[4].Files);
Assert.Single(result[4].Extras);
Assert.Equal("Ready Player One", result[4].Name);
}
[Fact]
public void TestAlternativeVersions()
{
var files = new[]
{
"Harry Potter and the Deathly Hallows/Chapter 1.ogg",
"Harry Potter and the Deathly Hallows/Chapter 1.mp3",
"Deadpool.mp3",
"Deadpool [HQ].mp3",
"Superman/audiobook.mp3",
"Superman/Superman.mp3",
"Superman/Superman [HQ].mp3",
"Superman/extra.mp3",
"Batman/ Chapter 1 .mp3",
"Batman/Chapter 1[loss-less].mp3"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
})).ToList();
Assert.Equal(5, result.Count);
// HP - Same name so we don't care which file is alternative
Assert.Single(result[0].AlternateVersions);
// DP
Assert.Empty(result[1].AlternateVersions);
// DP HQ (directory missing so we do not group deadpools together)
Assert.Empty(result[2].AlternateVersions);
// Superman
// Priority:
// 1. Name
// 2. audiobook
// 3. Names with modifiers
Assert.Equal(2, result[3].AlternateVersions.Count);
var paths = result[3].AlternateVersions.Select(x => x.Path).ToList();
Assert.Contains("Superman/audiobook.mp3", paths);
Assert.Contains("Superman/Superman [HQ].mp3", paths);
// Batman
Assert.Single(result[4].AlternateVersions);
}
[Fact]
public void TestNameYearExtraction()
{
var data = new[]
{
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(data.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i.Path
})).ToList();
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]
@ -82,9 +223,51 @@ namespace Jellyfin.Naming.Tests.AudioBook
Assert.Single(result);
}
[Fact]
public void TestWithoutFolder()
{
var files = new[]
{
"Harry Potter and the Deathly Hallows trailer.mp3"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
})).ToList();
Assert.Single(result);
}
[Fact]
public void TestEmpty()
{
var files = Array.Empty<string>();
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
})).ToList();
Assert.Empty(result);
}
private AudioBookListResolver GetResolver()
{
return new AudioBookListResolver(_namingOptions);
}
internal struct NameYearPath
{
public string Name;
public string Path;
public int? Year;
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
@ -14,30 +14,24 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
yield return new object[]
{
new AudioBookFileInfo()
{
Path = @"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
Container = "mp3",
}
new AudioBookFileInfo(
@"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
"mp3")
};
yield return new object[]
{
new AudioBookFileInfo()
{
Path = @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
Container = "ogg",
ChapterNumber = 1
}
new AudioBookFileInfo(
@"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
"ogg",
chapterNumber: 1)
};
yield return new object[]
{
new AudioBookFileInfo()
{
Path = @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
Container = "mp3",
ChapterNumber = 2,
PartNumber = 3
}
new AudioBookFileInfo(
@"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
"mp3",
chapterNumber: 2,
partNumber: 3)
};
}
@ -52,13 +46,22 @@ namespace Jellyfin.Naming.Tests.AudioBook
Assert.Equal(result!.Container, expectedResult.Container);
Assert.Equal(result!.ChapterNumber, expectedResult.ChapterNumber);
Assert.Equal(result!.PartNumber, expectedResult.PartNumber);
Assert.Equal(result!.IsDirectory, expectedResult.IsDirectory);
}
[Fact]
public void Resolve_EmptyFileName_ArgumentException()
public void Resolve_InvalidExtension()
{
Assert.Throws<ArgumentException>(() => new AudioBookResolver(_namingOptions).Resolve(string.Empty));
var result = new AudioBookResolver(_namingOptions).Resolve(@"/server/AudioBooks/Larry Potter/Larry Potter.mp9");
Assert.Null(result);
}
[Fact]
public void Resolve_EmptyFileName()
{
var result = new AudioBookResolver(_namingOptions).Resolve(string.Empty);
Assert.Null(result);
}
}
}

View File

@ -0,0 +1,36 @@
using Emby.Naming.Common;
using Xunit;
namespace Jellyfin.Naming.Tests.Common
{
public class NamingOptionsTest
{
[Fact]
public void TestNamingOptionsCompile()
{
var options = new NamingOptions();
Assert.NotEmpty(options.VideoFileStackingRegexes);
Assert.NotEmpty(options.CleanDateTimeRegexes);
Assert.NotEmpty(options.CleanStringRegexes);
Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes);
Assert.NotEmpty(options.EpisodeMultiPartRegexes);
}
[Fact]
public void TestNamingOptionsEpisodeExpressions()
{
var exp = new EpisodeExpression(string.Empty);
Assert.False(exp.IsOptimistic);
exp.IsOptimistic = true;
Assert.True(exp.IsOptimistic);
Assert.Equal(string.Empty, exp.Expression);
Assert.NotNull(exp.Regex);
exp.Expression = "test";
Assert.Equal("test", exp.Expression);
Assert.NotNull(exp.Regex);
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using Emby.Naming.Common;
using Emby.Naming.Subtitles;
using Xunit;
@ -26,21 +26,17 @@ namespace Jellyfin.Naming.Tests.Subtitles
Assert.Equal(language, result?.Language, true);
Assert.Equal(isDefault, result?.IsDefault);
Assert.Equal(isForced, result?.IsForced);
Assert.Equal(input, result?.Path);
}
[Theory]
[InlineData("The Skin I Live In (2011).mp4")]
[InlineData("")]
public void SubtitleParser_InvalidFileName_ReturnsNull(string input)
{
var parser = new SubtitleParser(_namingOptions);
Assert.Null(parser.ParseFile(input));
}
[Fact]
public void SubtitleParser_EmptyFileName_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new SubtitleParser(_namingOptions).ParseFile(string.Empty));
}
}
}

View File

@ -1,4 +1,4 @@
using Emby.Naming.Common;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Xunit;
@ -7,43 +7,98 @@ namespace Jellyfin.Naming.Tests.TV
public class EpisodePathParserTest
{
[Theory]
[InlineData("/media/Foo/Foo-S01E01", "Foo", 1, 1)]
[InlineData("/media/Foo - S04E011", "Foo", 4, 11)]
[InlineData("/media/Foo/Foo s01x01", "Foo", 1, 1)]
[InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", "Foo (2019)", 4, 3)]
[InlineData("D:\\media\\Foo\\Foo-S01E01", "Foo", 1, 1)]
[InlineData("D:\\media\\Foo - S04E011", "Foo", 4, 11)]
[InlineData("D:\\media\\Foo\\Foo s01x01", "Foo", 1, 1)]
[InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", "Foo (2019)", 4, 3)]
[InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", "Elementary", 2, 3)]
[InlineData("/Season 1/seriesname S01E02 blah.avi", "seriesname", 1, 2)]
[InlineData("/Running Man/Running Man S2017E368.mkv", "Running Man", 2017, 368)]
[InlineData("/Season 1/seriesname 01x02 blah.avi", "seriesname", 1, 2)]
[InlineData("/Season 25/The Simpsons.S25E09.Steal this episode.mp4", "The Simpsons", 25, 9)]
[InlineData("/Season 1/seriesname S01x02 blah.avi", "seriesname", 1, 2)]
[InlineData("/Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", "Elementary", 2, 3)]
[InlineData("/Season 1/seriesname S01xE02 blah.avi", "seriesname", 1, 2)]
[InlineData("/Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", "Elementary", 2, 3)]
[InlineData("/Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", "Elementary", 2, 3)]
[InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", "Elementary", 2, 3)]
[InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", "Elementary", 1, 23)]
[InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", "The Wonder Years", 4, 7)]
[InlineData("/media/Foo/Foo-S01E01", true, "Foo", 1, 1)]
[InlineData("/media/Foo - S04E011", true, "Foo", 4, 11)]
[InlineData("/media/Foo/Foo s01x01", true, "Foo", 1, 1)]
[InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", true, "Foo (2019)", 4, 3)]
[InlineData("D:\\media\\Foo\\Foo-S01E01", true, "Foo", 1, 1)]
[InlineData("D:\\media\\Foo - S04E011", true, "Foo", 4, 11)]
[InlineData("D:\\media\\Foo\\Foo s01x01", true, "Foo", 1, 1)]
[InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", true, "Foo (2019)", 4, 3)]
[InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", false, "Elementary", 2, 3)]
[InlineData("/Season 1/seriesname S01E02 blah.avi", false, "seriesname", 1, 2)]
[InlineData("/Running Man/Running Man S2017E368.mkv", false, "Running Man", 2017, 368)]
[InlineData("/Season 1/seriesname 01x02 blah.avi", false, "seriesname", 1, 2)]
[InlineData("/Season 25/The Simpsons.S25E09.Steal this episode.mp4", false, "The Simpsons", 25, 9)]
[InlineData("/Season 1/seriesname S01x02 blah.avi", false, "seriesname", 1, 2)]
[InlineData("/Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", false, "Elementary", 2, 3)]
[InlineData("/Season 1/seriesname S01xE02 blah.avi", false, "seriesname", 1, 2)]
[InlineData("/Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", false, "Elementary", 2, 3)]
[InlineData("/Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", false, "Elementary", 2, 3)]
[InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)]
[InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)]
[InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)]
// TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)]
// TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)]
// TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)]
// TODO: [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", "The Daily Show", 25, 22)]
// TODO: [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", "Watchmen (2019)", 1, 3)]
// TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", "The Legend of Condor Heroes 2017", 1, 7)]
public void ParseEpisodesCorrectly(string path, string name, int season, int episode)
public void ParseEpisodesCorrectly(string path, bool isDirectory, string name, int season, int episode)
{
NamingOptions o = new NamingOptions();
EpisodePathParser p = new EpisodePathParser(o);
var res = p.Parse(path, false);
var res = p.Parse(path, isDirectory);
Assert.True(res.Success);
Assert.Equal(name, res.SeriesName);
Assert.Equal(season, res.SeasonNumber);
Assert.Equal(episode, res.EpisodeNumber);
}
[Theory]
[InlineData("/test/01-03.avi", true, true)]
public void EpisodePathParserTest_DifferentExpressionsParameters(string path, bool? isNamed, bool? isOptimistic)
{
NamingOptions o = new NamingOptions();
EpisodePathParser p = new EpisodePathParser(o);
var res = p.Parse(path, false, isNamed, isOptimistic);
Assert.True(res.Success);
}
[Fact]
public void EpisodePathParserTest_FalsePositivePixelRate()
{
NamingOptions o = new NamingOptions();
EpisodePathParser p = new EpisodePathParser(o);
var res = p.Parse("Series Special (1920x1080).mkv", false);
Assert.False(res.Success);
}
[Fact]
public void EpisodeResolverTest_WrongExtension()
{
var res = new EpisodeResolver(new NamingOptions()).Resolve("test.mp3", false);
Assert.Null(res);
}
[Fact]
public void EpisodeResolverTest_WrongExtensionStub()
{
var res = new EpisodeResolver(new NamingOptions()).Resolve("dvd.disc", false);
Assert.NotNull(res);
Assert.True(res!.IsStub);
}
/*
* EpisodePathParser.cs:130 is currently unreachable, but the piece of code is useful and could be reached with addition of new EpisodeExpressions.
* In order to preserve it but achieve 100% code coverage the test case below with made up expressions and filename is used.
*/
[Fact]
public void EpisodePathParserTest_EmptyDateParsers()
{
NamingOptions o = new NamingOptions()
{
EpisodeExpressions = new[] { new EpisodeExpression("(([0-9]{4})-([0-9]{2})-([0-9]{2}) [0-9]{2}:[0-9]{2}:[0-9]{2})", true) }
};
o.Compile();
EpisodePathParser p = new EpisodePathParser(o);
var res = p.Parse("ABC_2019_10_21 11:00:00", false);
Assert.True(res.Success);
}
}
}

View File

@ -74,7 +74,7 @@ namespace Jellyfin.Naming.Tests.TV
var result = new EpisodePathParser(options)
.Parse(filename, false);
Assert.Equal(result.EndingEpsiodeNumber, endingEpisodeNumber);
Assert.Equal(result.EndingEpisodeNumber, endingEpisodeNumber);
}
}
}

View File

@ -1,4 +1,4 @@
using Emby.Naming.TV;
using Emby.Naming.TV;
using Xunit;
namespace Jellyfin.Naming.Tests.TV
@ -6,26 +6,30 @@ namespace Jellyfin.Naming.Tests.TV
public class SeasonFolderTests
{
[Theory]
[InlineData(@"/Drive/Season 1", 1)]
[InlineData(@"/Drive/Season 2", 2)]
[InlineData(@"/Drive/Season 02", 2)]
[InlineData(@"/Drive/Seinfeld/S02", 2)]
[InlineData(@"/Drive/Seinfeld/2", 2)]
[InlineData(@"/Drive/Season 2009", 2009)]
[InlineData(@"/Drive/Season1", 1)]
[InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4)]
[InlineData(@"/Drive/Season 7 (2016)", 7)]
[InlineData(@"/Drive/Staffel 7 (2016)", 7)]
[InlineData(@"/Drive/Stagione 7 (2016)", 7)]
[InlineData(@"/Drive/Season (8)", null)]
[InlineData(@"/Drive/3.Staffel", 3)]
[InlineData(@"/Drive/s06e05", null)]
[InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null)]
public void GetSeasonNumberFromPathTest(string path, int? seasonNumber)
[InlineData(@"/Drive/Season 1", 1, true)]
[InlineData(@"/Drive/Season 2", 2, true)]
[InlineData(@"/Drive/Season 02", 2, true)]
[InlineData(@"/Drive/Seinfeld/S02", 2, true)]
[InlineData(@"/Drive/Seinfeld/2", 2, true)]
[InlineData(@"/Drive/Season 2009", 2009, true)]
[InlineData(@"/Drive/Season1", 1, true)]
[InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
[InlineData(@"/Drive/Season 7 (2016)", 7, false)]
[InlineData(@"/Drive/Staffel 7 (2016)", 7, false)]
[InlineData(@"/Drive/Stagione 7 (2016)", 7, false)]
[InlineData(@"/Drive/Season (8)", null, false)]
[InlineData(@"/Drive/3.Staffel", 3, false)]
[InlineData(@"/Drive/s06e05", null, false)]
[InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
[InlineData(@"/Drive/extras", 0, true)]
[InlineData(@"/Drive/specials", 0, true)]
public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
{
var result = SeasonPathParser.Parse(path, true, true);
Assert.Equal(result.SeasonNumber != null, result.Success);
Assert.Equal(result.SeasonNumber, seasonNumber);
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
}
}
}

View File

@ -1,4 +1,5 @@
using Emby.Naming.Common;
using System.IO;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Xunit;
@ -15,7 +16,6 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("/server/The Walking Dead 4x01.mp4", "The Walking Dead", 4, 1)]
[InlineData("/server/the_simpsons-s02e01_18536.mp4", "the_simpsons", 2, 1)]
[InlineData("/server/Temp/S01E02 foo.mp4", "", 1, 2)]
[InlineData("Series/4-12 - The Woman.mp4", "", 4, 12)]
[InlineData("Series/4x12 - The Woman.mp4", "", 4, 12)]
[InlineData("Series/LA X, Pt. 1_s06e32.mp4", "LA X, Pt. 1", 6, 32)]
[InlineData("[Baz-Bar]Foo - [1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
@ -24,16 +24,37 @@ namespace Jellyfin.Naming.Tests.TV
// TODO: [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
// TODO: [InlineData("E:\\Anime\\Yahari Ore no Seishun Love Comedy wa Machigatteiru\\Yahari Ore no Seishun Love Comedy wa Machigatteiru. Zoku\\Oregairu Zoku 11 - Hayama Hayato Always Renconds to Everyone's Expectations..mkv", "Yahari Ore no Seishun Love Comedy wa Machigatteiru", null, 11)]
// TODO: [InlineData(@"/Library/Series/The Grand Tour (2016)/Season 1/S01E01 The Holy Trinity.mkv", "The Grand Tour", 1, 1)]
public void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber)
public void TestSimple(string path, string seriesName, int? seasonNumber, int? episodeNumber)
{
Test(path, seriesName, seasonNumber, episodeNumber, null);
}
[Theory]
[InlineData("Series/4-12 - The Woman.mp4", "", 4, 12, 12)]
public void TestWithPossibleEpisodeEnd(string path, string seriesName, int? seasonNumber, int? episodeNumber, int? episodeEndNumber)
{
Test(path, seriesName, seasonNumber, episodeNumber, episodeEndNumber);
}
private void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber, int? episodeEndNumber)
{
var options = new NamingOptions();
var result = new EpisodeResolver(options)
.Resolve(path, false);
Assert.NotNull(result);
Assert.Equal(seasonNumber, result?.SeasonNumber);
Assert.Equal(episodeNumber, result?.EpisodeNumber);
Assert.Equal(seriesName, result?.SeriesName, true);
Assert.Equal(path, result?.Path);
Assert.Equal(Path.GetExtension(path).Substring(1), result?.Container);
Assert.Null(result?.Format3D);
Assert.False(result?.Is3D);
Assert.False(result?.IsStub);
Assert.Null(result?.StubType);
Assert.Equal(episodeEndNumber, result?.EndingEpisodeNumber);
Assert.False(result?.IsByDate);
}
}
}

View File

@ -1,4 +1,4 @@
using System.IO;
using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Xunit;
@ -51,6 +51,8 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)]
[InlineData("My Movie 20131209", "My Movie 20131209", null)]
[InlineData("My Movie 2013-12-09 2013", "My Movie 2013-12-09", 2013)]
[InlineData(null, null, null)]
[InlineData("", "", null)]
public void CleanDateTimeTest(string input, string expectedName, int? expectedYear)
{
input = Path.GetFileName(input);

View File

@ -1,7 +1,9 @@
using Emby.Naming.Common;
using System;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
using Xunit;
using MediaType = Emby.Naming.Common.MediaType;
namespace Jellyfin.Naming.Tests.Video
{
@ -93,6 +95,23 @@ namespace Jellyfin.Naming.Tests.Video
}
}
[Fact]
public void TestExtraInfo_InvalidRuleType()
{
var rule = new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, @"([eE]x(tra)?\.\w+)", MediaType.Video);
var options = new NamingOptions { VideoExtraRules = new[] { rule } };
var res = GetExtraTypeParser(options).GetExtraInfo("extra.mp4");
Assert.Equal(rule, res.Rule);
}
[Fact]
public void TestFlagsParser()
{
var flags = new FlagParser(_videoOptions).GetFlags(string.Empty);
Assert.Empty(flags);
}
private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
{
return new ExtraResolver(videoOptions);

View File

@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.IO;
@ -11,8 +12,8 @@ namespace Jellyfin.Naming.Tests.Video
private readonly NamingOptions _namingOptions = new NamingOptions();
// FIXME
// [Fact]
private void TestMultiEdition1()
[Fact]
public void TestMultiEdition1()
{
var files = new[]
{
@ -35,8 +36,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
// [Fact]
private void TestMultiEdition2()
[Fact]
public void TestMultiEdition2()
{
var files = new[]
{
@ -81,8 +82,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
// [Fact]
private void TestLetterFolders()
[Fact]
public void TestLetterFolders()
{
var files = new[]
{
@ -109,8 +110,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
// [Fact]
private void TestMultiVersionLimit()
[Fact]
public void TestMultiVersionLimit()
{
var files = new[]
{
@ -138,8 +139,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
// [Fact]
private void TestMultiVersionLimit2()
[Fact]
public void TestMultiVersionLimit2()
{
var files = new[]
{
@ -168,8 +169,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
// [Fact]
private void TestMultiVersion3()
[Fact]
public void TestMultiVersion3()
{
var files = new[]
{
@ -194,8 +195,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
// [Fact]
private void TestMultiVersion4()
[Fact]
public void TestMultiVersion4()
{
// Test for false positive
@ -221,9 +222,8 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Empty(result[0].AlternateVersions);
}
// FIXME
// [Fact]
private void TestMultiVersion5()
[Fact]
public void TestMultiVersion5()
{
var files = new[]
{
@ -254,8 +254,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
// [Fact]
private void TestMultiVersion6()
[Fact]
public void TestMultiVersion6()
{
var files = new[]
{
@ -285,9 +285,8 @@ namespace Jellyfin.Naming.Tests.Video
Assert.True(result[0].AlternateVersions[5].Is3D);
}
// FIXME
// [Fact]
private void TestMultiVersion7()
[Fact]
public void TestMultiVersion7()
{
var files = new[]
{
@ -306,12 +305,9 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(2, result.Count);
}
// FIXME
// [Fact]
private void TestMultiVersion8()
[Fact]
public void TestMultiVersion8()
{
// This is not actually supported yet
var files = new[]
{
@"/movies/Iron Man/Iron Man.mkv",
@ -339,9 +335,8 @@ namespace Jellyfin.Naming.Tests.Video
Assert.True(result[0].AlternateVersions[4].Is3D);
}
// FIXME
// [Fact]
private void TestMultiVersion9()
[Fact]
public void TestMultiVersion9()
{
// Test for false positive
@ -367,9 +362,8 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Empty(result[0].AlternateVersions);
}
// FIXME
// [Fact]
private void TestMultiVersion10()
[Fact]
public void TestMultiVersion10()
{
var files = new[]
{
@ -390,12 +384,9 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Single(result[0].AlternateVersions);
}
// FIXME
// [Fact]
private void TestMultiVersion11()
[Fact]
public void TestMultiVersion11()
{
// Currently not supported but we should probably handle this.
var files = new[]
{
@"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
@ -415,6 +406,16 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Single(result[0].AlternateVersions);
}
[Fact]
public void TestEmptyList()
{
var resolver = GetResolver();
var result = resolver.Resolve(new List<FileSystemMetadata>()).ToList();
Assert.Empty(result);
}
private VideoListResolver GetResolver()
{
return new VideoListResolver(_namingOptions);

View File

@ -1,4 +1,4 @@
using Emby.Naming.Common;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Xunit;
@ -23,6 +23,7 @@ namespace Jellyfin.Naming.Tests.Video
Test("video.hdtv.disc", true, "tv");
Test("video.pdtv.disc", true, "tv");
Test("video.dsr.disc", true, "tv");
Test(string.Empty, false, "tv");
}
[Fact]

View File

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.IO;
@ -369,6 +369,26 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Single(result);
}
[Fact]
public void TestFourRooms()
{
var files = new[]
{
@"Four Rooms - A.avi",
@"Four Rooms - A.mp4"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
Assert.Equal(2, result.Count);
}
[Fact]
public void TestMovieTrailer()
{
@ -431,6 +451,13 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Single(result);
}
[Fact]
public void TestDirectoryStack()
{
var stack = new FileStack();
Assert.False(stack.ContainsFile("XX", true));
}
private VideoListResolver GetResolver()
{
return new VideoListResolver(_namingOptions);

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
@ -14,165 +15,135 @@ namespace Jellyfin.Naming.Tests.Video
{
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
Container = "mkv",
Name = "7 Psychos"
}
new VideoFileInfo(
path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
container: "mkv",
name: "7 Psychos")
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
Container = "mkv",
Name = "3 days to kill",
Year = 2005
}
new VideoFileInfo(
path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
container: "mkv",
name: "3 days to kill",
year: 2005)
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/American Psycho/American.Psycho.mkv",
Container = "mkv",
Name = "American.Psycho",
}
new VideoFileInfo(
path: @"/server/Movies/American Psycho/American.Psycho.mkv",
container: "mkv",
name: "American.Psycho")
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
Container = "mkv",
Name = "brave",
Year = 2006,
Is3D = true,
Format3D = "sbs",
}
new VideoFileInfo(
path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
container: "mkv",
name: "brave",
year: 2006,
is3D: true,
format3D: "sbs")
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
Container = "mkv",
Name = "300",
Year = 2006
}
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
container: "mkv",
name: "300",
year: 2006)
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
Container = "mkv",
Name = "300",
Year = 2006,
Is3D = true,
Format3D = "sbs",
}
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
container: "mkv",
name: "300",
year: 2006,
is3D: true,
format3D: "sbs")
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
Container = "disc",
Name = "brave",
Year = 2006,
IsStub = true,
StubType = "bluray",
}
new VideoFileInfo(
path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
container: "disc",
name: "brave",
year: 2006,
isStub: true,
stubType: "bluray")
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
Container = "disc",
Name = "300",
Year = 2006,
IsStub = true,
StubType = "bluray",
}
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
container: "disc",
name: "300",
year: 2006,
isStub: true,
stubType: "bluray")
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
Container = "disc",
Name = "Brave",
Year = 2006,
IsStub = true,
StubType = "bluray",
}
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
container: "disc",
name: "Brave",
year: 2006,
isStub: true,
stubType: "bluray")
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
Container = "disc",
Name = "300",
Year = 2006,
IsStub = true,
StubType = "bluray",
}
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
container: "disc",
name: "300",
year: 2006,
isStub: true,
stubType: "bluray")
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
Container = "mkv",
Name = "300",
Year = 2006,
ExtraType = ExtraType.Trailer,
}
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
container: "mkv",
name: "300",
year: 2006,
extraType: ExtraType.Trailer)
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
Container = "mkv",
Name = "Brave",
Year = 2006,
ExtraType = ExtraType.Trailer,
}
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
container: "mkv",
name: "Brave",
year: 2006,
extraType: ExtraType.Trailer)
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/300 (2007)/300 (2006).mkv",
Container = "mkv",
Name = "300",
Year = 2006
}
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).mkv",
container: "mkv",
name: "300",
year: 2006)
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
Container = "mkv",
Name = "Bad Boys",
Year = 1995,
}
new VideoFileInfo(
path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
container: "mkv",
name: "Bad Boys",
year: 1995)
};
yield return new object[]
{
new VideoFileInfo()
{
Path = @"/server/Movies/Brave (2007)/Brave (2006).mkv",
Container = "mkv",
Name = "Brave",
Year = 2006,
}
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006).mkv",
container: "mkv",
name: "Brave",
year: 2006)
};
}
@ -194,6 +165,34 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(result?.StubType, expectedResult.StubType);
Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory);
Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
Assert.Equal(result?.ToString(), expectedResult.ToString());
}
[Fact]
public void ResolveFile_EmptyPath()
{
var result = new VideoResolver(_namingOptions).ResolveFile(string.Empty);
Assert.Null(result);
}
[Fact]
public void ResolveDirectoryTest()
{
var paths = new[]
{
@"/Server/Iron Man",
@"Batman",
string.Empty
};
var resolver = new VideoResolver(_namingOptions);
var results = paths.Select(path => resolver.ResolveDirectory(path)).ToList();
Assert.Equal(3, results.Count);
Assert.NotNull(results[0]);
Assert.NotNull(results[1]);
Assert.Null(results[2]);
}
}
}