#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 { public class NamingOptions { public NamingOptions() { VideoFileExtensions = new[] { ".m4v", ".3gp", ".nsv", ".ts", ".ty", ".strm", ".rm", ".rmvb", ".ifo", ".mov", ".qt", ".divx", ".xvid", ".bivx", ".vob", ".nrg", ".img", ".iso", ".pva", ".wmv", ".asf", ".asx", ".ogm", ".m2v", ".avi", ".bin", ".dvr-ms", ".mpg", ".mpeg", ".mp4", ".mkv", ".avc", ".vp3", ".svq3", ".nuv", ".viv", ".dv", ".fli", ".flv", ".001", ".tp" }; VideoFlagDelimiters = new[] { '(', ')', '-', '.', '_', '[', ']' }; StubFileExtensions = new[] { ".disc" }; 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") }; VideoFileStackingExpressions = new[] { "(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$", "(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(.*?)(\\.[^.]+)$", "(.*?)([ ._-]*[a-d])(.*?)(\\.[^.]+)$" }; CleanDateTimes = new[] { @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*" }; 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|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"(\[.*\])" }; SubtitleFileExtensions = new[] { ".srt", ".ssa", ".ass", ".sub" }; SubtitleFlagDelimiters = new[] { '.' }; SubtitleForcedFlags = new[] { "foreign", "forced" }; SubtitleDefaultFlags = new[] { "default" }; AlbumStackingPrefixes = new[] { "disc", "cd", "disk", "vol", "volume" }; AudioFileExtensions = new[] { ".nsv", ".m4a", ".flac", ".aac", ".strm", ".pls", ".rm", ".mpa", ".wav", ".wma", ".ogg", ".opus", ".mp3", ".mp2", ".mod", ".amf", ".669", ".dmf", ".dsm", ".far", ".gdm", ".imf", ".it", ".m15", ".med", ".okt", ".s3m", ".stm", ".sfx", ".ult", ".uni", ".xm", ".sid", ".ac3", ".dts", ".cue", ".aif", ".aiff", ".ape", ".mac", ".mpc", ".mp+", ".mpp", ".shn", ".wv", ".nsf", ".spc", ".gym", ".adplug", ".adx", ".dsp", ".adp", ".ymf", ".ast", ".afc", ".hps", ".xsp", ".acc", ".m4b", ".oga", ".dsf", ".mka" }; EpisodeExpressions = new[] { // *** Begin Kodi Standard Naming // new EpisodeExpression(@".*(\\|\/)(?((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?[0-9]+)[][ ._-]*[Ee](?[0-9]+)([^\\/]*)$") { IsNamed = true }, // new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true) { DateTimeFormats = new[] { "yyyy.MM.dd", "yyyy-MM-dd", "yyyy_MM_dd" } }, new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true) { DateTimeFormats = new[] { "dd.MM.yyyy", "dd-MM-yyyy", "dd_MM_yyyy" } }, // This isn't a Kodi naming rule, but the expression below causes false positives, // so we make sure this one gets tested first. // "Foo Bar 889" new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?[\w\s]+?)\s(?[0-9]{1,3})(-(?[0-9]{2,3}))*[^\\\/x]*$") { IsNamed = true }, new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, new EpisodeExpression(@"[\\\\/\\._ -](?(?![0-9]+[0-9][0-9])([^\\\/])*)[\\\\/\\._ -](?[0-9]+)(?[0-9][0-9](?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([\\._ -][^\\\\/]*)$") { IsOptimistic = true, IsNamed = true, SupportsAbsoluteEpisodeNumbers = false }, new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, // *** End Kodi Standard Naming // [bar] Foo - 1 [baz] new EpisodeExpression(@".*?(\[.*?\])+.*?(?[\w\s]+?)[-\s_]+(?[0-9]+).*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)[sS]?(?[0-9]+)[xX](?[0-9]+)[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)[sS](?[0-9]+)[x,X]?[eE](?[0-9]+)[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)(?((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?[0-9]{1,4})[xX](?[0-9]+))[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)(?[^\\\/]*)[sS](?[0-9]{1,4})[xX\.]?[eE](?[0-9]+)[^\\\/]*$") { IsNamed = true }, // "01.avi" new EpisodeExpression(@".*[\\\/](?[0-9]+)(-(?[0-9]+))*\.\w+$") { IsOptimistic = true, IsNamed = true }, // "1-12 episode title" new EpisodeExpression(@"([0-9]+)-([0-9]+)"), // "01 - blah.avi", "01-blah.avi" new EpisodeExpression(@".*(\\|\/)(?[0-9]{1,3})(-(?[0-9]{2,3}))*\s?-\s?[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01.blah.avi" new EpisodeExpression(@".*(\\|\/)(?[0-9]{1,3})(-(?[0-9]{2,3}))*\.[^\\\/]+$") { IsOptimistic = true, IsNamed = true }, // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah" new EpisodeExpression(@".*[\\\/][^\\\/]* - (?[0-9]{1,3})(-(?[0-9]{2,3}))*[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01 episode title.avi" new EpisodeExpression(@"[Ss]eason[\._ ](?[0-9]+)[\\\/](?[0-9]{1,3})([^\\\/]*)$") { IsOptimistic = true, IsNamed = true }, // "Episode 16", "Episode 16 - Title" new EpisodeExpression(@".*[\\\/][^\\\/]* (?[0-9]{1,3})(-(?[0-9]{2,3}))*[^\\\/]*$") { IsOptimistic = true, IsNamed = true } }; EpisodeWithoutSeasonExpressions = new[] { @"[/\._ \-]()([0-9]+)(-[0-9]+)?" }; EpisodeMultiPartExpressions = new[] { @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)" }; VideoExtraRules = new[] { 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( 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 @"ch(?:apter)?[\s_-]?(?[0-9]+)", // Detect specified parts, like Part 02 @"p(?:ar)?t[\s_-]?(?[0-9]+)", // Chapter is often beginning of filename "^(?[0-9]+)", // Part if often ending of filename "(?[0-9]+)$", // Sometimes named as 0001_005 (chapter_part) "(?[0-9]+)_(?[0-9]+)", // Some audiobooks are ripped from cd's, and will be named by disk number. @"dis(?:c|k)[\s_-]?(?[0-9]+)" }; var extensions = VideoFileExtensions.ToList(); extensions.AddRange(new[] { ".mkv", ".m2t", ".m2ts", ".img", ".iso", ".mk3d", ".ts", ".rmvb", ".mov", ".avi", ".mpg", ".mpeg", ".wmv", ".mp4", ".divx", ".dvr-ms", ".wtv", ".ogm", ".ogv", ".asf", ".m4v", ".flv", ".f4v", ".3gp", ".webm", ".mts", ".m2v", ".rec", ".mxf" }); MultipleEpisodeExpressions = new[] { @".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})((-| - )?[xXeE](?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3})(-[xE]?[eE]?(?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3}))((-| - )?[xXeE](?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?[0-9]{1,4})[xX](?[0-9]{1,3}))(-[xX]?[eE]?(?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?[^\\\/]*)[sS](?[0-9]{1,4})[xX\.]?[eE](?[0-9]{1,3})((-| - )?[xXeE](?[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?[^\\\/]*)[sS](?[0-9]{1,4})[xX\.]?[eE](?[0-9]{1,3})(-[xX]?[eE]?(?[0-9]{1,3}))+[^\\\/]*$" }.Select(i => new EpisodeExpression(i) { IsNamed = true }).ToArray(); VideoFileExtensions = extensions .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); Compile(); } public string[] AudioFileExtensions { get; set; } public string[] AlbumStackingPrefixes { get; set; } public string[] SubtitleFileExtensions { get; set; } public char[] SubtitleFlagDelimiters { get; set; } public string[] SubtitleForcedFlags { get; set; } public string[] SubtitleDefaultFlags { get; set; } public EpisodeExpression[] EpisodeExpressions { get; set; } public string[] EpisodeWithoutSeasonExpressions { get; set; } public string[] EpisodeMultiPartExpressions { get; set; } public string[] VideoFileExtensions { get; set; } public string[] StubFileExtensions { get; set; } public string[] AudioBookPartsExpressions { get; set; } public StubTypeRule[] StubTypes { get; set; } public char[] VideoFlagDelimiters { get; set; } public Format3DRule[] Format3DRules { get; set; } public string[] VideoFileStackingExpressions { get; set; } public string[] CleanDateTimes { get; set; } public string[] CleanStrings { get; set; } public EpisodeExpression[] MultipleEpisodeExpressions { get; set; } public ExtraRule[] VideoExtraRules { get; set; } public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty(); public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty(); public Regex[] CleanStringRegexes { get; private set; } = Array.Empty(); public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty(); public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty(); public void Compile() { VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray(); CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray(); EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray(); } private Regex Compile(string exp) { return new Regex(exp, RegexOptions.IgnoreCase | RegexOptions.Compiled); } } }