public static FileType CheckFileType(string extension) { if (!string.IsNullOrEmpty(extension)) { FileExtension file = new FileExtension(extension.ToUpper(), FileType.UnKnow, ""); if (VideoFileExtensions.Contains(file)) { return(FileType.Video); } else if (SubTitleFileExtensions.Contains(file)) { return(FileType.SubTitle); } else if (AudioFileExtensions.Contains(file)) { return(FileType.Audio); } else if (ImageFileExtensions.Contains(file)) { return(FileType.Image); } } return(FileType.UnKnow); }
private async Task <Either <BaseError, Unit> > ScanEpisodes( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, Season season, string seasonPath, CancellationToken cancellationToken) { var allSeasonFiles = _localFileSystem.ListSubdirectories(seasonPath) .Map(_localFileSystem.ListFiles) .Flatten() .Append(_localFileSystem.ListFiles(seasonPath)) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => !Path.GetFileName(f).StartsWith("._")) .OrderBy(identity) .ToList(); foreach (string file in allSeasonFiles) { // TODO: figure out how to rebuild playlists Either <BaseError, Episode> maybeEpisode = await _televisionRepository .GetOrAddEpisode(season, libraryPath, file) .BindT( episode => UpdateStatistics(new MediaItemScanResult <Episode>(episode), ffmpegPath, ffprobePath) .MapT(_ => episode)) .BindT(UpdateMetadata) .BindT(e => UpdateThumbnail(e, cancellationToken)) .BindT(UpdateSubtitles) .BindT(e => FlagNormal(new MediaItemScanResult <Episode>(e))) .MapT(r => r.Item); foreach (BaseError error in maybeEpisode.LeftToSeq()) { _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value); } foreach (Episode episode in maybeEpisode.RightToSeq()) { await _searchIndex.UpdateItems(_searchRepository, new List <MediaItem> { episode }); } } // TODO: remove missing episodes? return(Unit.Default); }
/// <summary> /// Gets the files from directory. /// </summary> /// <param name="dir">The dir.</param> private void GetFilesFromDirectory(string dir) { // files for the current folder only List <string> files = new List <string>(); try { // ignore the folder? if (File.Exists(Path.Combine(dir, "tv.ignore"))) { return; } // add files only files = Directory.GetFiles(dir).ToList(); } catch { return; } // Add files to list. foreach (var file in files) { string extension = "*" + Path.GetExtension(file); if (extension != "*." && VideoFileExtensions.Contains(extension)) { FileCacheEntities.Add(new FileCacheEntity() { FileNameOnly = Path.GetFileNameWithoutExtension(file), FullPathAndName = file }); } } // Do same for child folders. foreach (var childDir in Directory.GetDirectories(dir)) { GetFilesFromDirectory(childDir); } }
private async Task <Unit> ScanEpisodes( LibraryPath libraryPath, string ffprobePath, Season season, string seasonPath) { foreach (string file in _localFileSystem.ListFiles(seasonPath) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))).OrderBy(identity)) { // TODO: figure out how to rebuild playlists Either <BaseError, Episode> maybeEpisode = await _televisionRepository .GetOrAddEpisode(season, libraryPath, file) .BindT( episode => UpdateStatistics(new MediaItemScanResult <Episode>(episode), ffprobePath) .MapT(_ => episode)) .BindT(UpdateMetadata) .BindT(UpdateThumbnail); maybeEpisode.IfLeft( error => _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value)); } return(Unit.Default); }
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]|).*(19[0-9]{2}|20[0-9]{2})*", @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[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 // <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 --> new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$") { IsNamed = true }, // <!-- foo.ep01, foo.EP_01 --> 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)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$") { IsNamed = true }, new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$") { 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]))?)([\\._ -][^\\\\/]*)$") { 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(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, // "01.avi" new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[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(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01.blah.avi" new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[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(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01 episode title.avi" new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$") { IsOptimistic = true, IsNamed = true }, // "Episode 16", "Episode 16 - Title" new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[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 = 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, }, }; 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" } }; AudioBookPartsExpressions = new[] { // Detect specified chapters, like CH 01 @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)", // Detect specified parts, like Part 02 @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)", // Chapter is often beginning of filename "^(?<chapter>[0-9]+)", // Part if often ending of filename "(?<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]+)" }; 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 string[] { @".*(\\|\/)[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}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][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}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$" }.Select(i => new EpisodeExpression(i) { IsNamed = true }).ToArray(); VideoFileExtensions = extensions .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); Compile(); }
/// <summary> /// Initializes a new instance of the <see cref="NamingOptions"/> class. /// </summary> 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") }; VideoFileStackingRules = new[] { new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true), new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false), new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false) }; CleanDateTimes = new[] { @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](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[] { @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"^(?<cleaned>.+?)(\[.*\])", @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)", @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$" }; SubtitleFileExtensions = new[] { ".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 // <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 --> new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$") { IsNamed = true }, // <!-- foo.ep01, foo.EP_01 --> new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), // <!-- foo.E01., foo.e01. --> new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"), new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true) { DateTimeFormats = new[] { "yyyy.MM.dd", "yyyy-MM-dd", "yyyy_MM_dd" } }, new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[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)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$") { IsNamed = true }, new EpisodeExpression(@"[\\\/\._ \[\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\.[1-9])(?![0-9]))?)([^\\\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names // [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$") { IsNamed = true }, // /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, SupportsAbsoluteEpisodeNumbers = false }, new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, // *** End Kodi Standard Naming // "Episode 16", "Episode 16 - Title" new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, // "01.avi" new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[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(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01.blah.avi" new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[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(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01 episode title.avi" new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$") { IsOptimistic = true, IsNamed = true }, // Series and season only expression // "the show/season 1", "the show/s01" new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)") { IsNamed = true }, // Series and season only expression // "the show S01", "the show season 1" new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)") { IsNamed = true }, }; EpisodeWithoutSeasonExpressions = new[] { @"[/\._ \-]()([0-9]+)(-[0-9]+)?" }; EpisodeMultiPartExpressions = new[] { @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)" }; VideoExtraRules = new[] { new ExtraRule( ExtraType.Trailer, ExtraRuleType.DirectoryName, "trailers", 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.ThemeVideo, ExtraRuleType.DirectoryName, "backdrops", MediaType.Video), new ExtraRule( ExtraType.ThemeSong, ExtraRuleType.Filename, "theme", MediaType.Audio), new ExtraRule( ExtraType.ThemeSong, ExtraRuleType.DirectoryName, "theme-music", MediaType.Audio), new ExtraRule( ExtraType.Scene, ExtraRuleType.Suffix, "-scene", 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.DeletedScene, ExtraRuleType.Suffix, "-deletedscene", 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_-]?(?<chapter>[0-9]+)", // Detect specified parts, like Part 02 @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)", // Chapter is often beginning of filename "^(?<chapter>[0-9]+)", // Part if often ending of filename @"(?<!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>[0-9]{4})\s*\)\s*$", @"^\s*(?<name>[^ ].*?)\s*$" }; 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]?(?<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}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][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}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$" }.Select(i => new EpisodeExpression(i) { IsNamed = true }).ToArray(); VideoFileExtensions = extensions .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); AllExtrasTypesFolderNames = new Dictionary <string, ExtraType>(StringComparer.OrdinalIgnoreCase) { ["trailers"] = ExtraType.Trailer, ["theme-music"] = ExtraType.ThemeSong, ["backdrops"] = ExtraType.ThemeVideo, ["extras"] = ExtraType.Unknown, ["behind the scenes"] = ExtraType.BehindTheScenes, ["deleted scenes"] = ExtraType.DeletedScene, ["interviews"] = ExtraType.Interview, ["scenes"] = ExtraType.Scene, ["samples"] = ExtraType.Sample, ["shorts"] = ExtraType.Clip, ["featurettes"] = ExtraType.Clip }; Compile(); }
public ExtendedNamingOptions() { 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" }); // Problematic. Can always become configurable if needed. extensions.Remove(".dat"); extensions.Remove(".wpl"); extensions.Remove(".m3u"); VideoFileExtensions = extensions .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); VideoExtraRules.AddRange(new List <ExtraRule> { new ExtraRule { ExtraType = "scene", RuleType = ExtraRuleType.Suffix, Token = "-scene", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "clip", RuleType = ExtraRuleType.Suffix, Token = "-clip", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "interview", RuleType = ExtraRuleType.Suffix, Token = "-interview", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "behindthescenes", RuleType = ExtraRuleType.Suffix, Token = "-behindthescenes", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "deletedscene", RuleType = ExtraRuleType.Suffix, Token = "-deleted", MediaType = MediaType.Video } }); Format3DRules.AddRange(new List <Format3DRule> { // 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" } }); }
/// <summary> /// Initializes a new instance of the <see cref="NamingOptions"/> class. /// </summary> 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[] { "(?<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[] { @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](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[] { @"^(?:(?<header>\[雪飘工作室]\[|\[FLsnow]\[|\[Nekomoe\ kissaten]\[|\[LKSUB]\[|\[UHA-WINGS]\[|\[BDRIP]\[|\[YG&Neo\.sub]\[|\[KTXP]\[)(?<seriesname>[^[\]【]+)|(?:(?<seriesname>\d+[ .][^[\]【]+)\.bhd)|(?:【[^】]*】|\[[^\]]*])*(?<seriesname>[^[\]【]+?)(?:第[一二三四五六七八九1-9]季[ ]?)?(?: - \d+)?(?:\(480P.*)?(?:【[^】]*】|\[[^\]]*])*$|^\[(?<seriesname>[^\]]*)])" }; 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[] { // 用.fsx拼接编写的针对TV目录,适配各种中文格式的超长正则表达式 new EpisodeExpression(@"^(?:.*[\\/][[【](?<header>Nekomoe kissaten|LKSUB|UHA-WINGS|YG&Neo.sub|KTXP|動畫瘋|HYSUB|UHA-WINGS|Nekomoe kissaten&VCB-Studio|BDRIP)[\]】]\[?\ ?(?<seriesname>.+?)\ ?(?:第(?<seasonnumber>[一二三四五六七八九十1-9])季[ ]?|[Ss](?<seasonnumber>[1-9]))?(?<specialSeason>\[特別篇])?(?:\[年齡限制版])?(?:\ (?<epnumber>[0-9.]+)\ |]?\[(?<epnumber>[0-9.]+)])?(?:\[[^\]]*])*\.[A-z1-9]+|(?<seriesname>.+?)[\\/]\k<seriesname>\ (?<epnumber>[0-9.]+)\.[A-z1-9]+|(?<specialSeason>OVA|OAD|SP|特别篇|特別篇|【?剧场版】?)?(?<seriesname>.*?)\ ?(?:TV|MV|1080P|720P)?(?:第(?<seasonnumber>[一二三四五六七八九十1-9])季[ ]?|(?<seasonnumber>[1-9])|(?<seasonnumber>[Ⅰ-Ⅹ]))?(?<specialSeason>OVA|OAD|SP|特别篇|特別篇|【?剧场版】?)?(?:\[[^\]]*])*(?:\d{4})?[\\/](?:【漫锋网】)?(?:TV|MV|1080P|720P)?[\ _]?(?:第(?<seasonnumber>[一二三四五六七八九十1-9])季[ ]?)?(?:(?:(?:\[.*?])?\[)?(?:(?<specialSeason>OVA|OAD|SP|特别篇|特別篇|【?剧场版】?)|OP-ED|PV-CF|MV|AD|\ The\ Movie\ 简体).*?)?(?:(?<=[\\/ ])Menu)?(?:(?<=[\\/ ]|EP|OVA|OAD|SP|特别篇|特別篇|【?剧场版】?|[-_\u4e00-\u9fa5])(?<epnumber>[0-9.]+))?(?:\[[^\]]*])*(?:.*?\.bhd)?(?:\..*?)?(?:\ \(.*?)?\.[A-z1-9]+|(?<specialSeason>OVA|OAD|SP|特别篇|特別篇|【?剧场版】?)?.*?\ ?(?:TV|MV|1080P|720P)?(?:第(?<seasonnumber>[一二三四五六七八九十1-9])季[ ]?|(?<seasonnumber>[1-9])|(?<seasonnumber>[Ⅰ-Ⅹ]))?(?<specialSeason>OVA|OAD|SP|特别篇|特別篇|【?剧场版】?)?(?:\[[^\]]*])*(?:\d{4})?[\\/](?:【漫锋网】)?(?<seriesname>.*?)(?:TV|MV|1080P|720P)?[\ _]?(?:第(?<seasonnumber>[一二三四五六七八九十1-9])季[ ]?)?(?:(?:(?:\[.*?])?\[)?(?:(?<specialSeason>OVA|OAD|SP|特别篇|特別篇|【?剧场版】?)|OP-ED|PV-CF|MV|AD|\ The\ Movie\ 简体).*?)?(?:(?<=[\\/ ])Menu)?(?:(?<=[\\/ ]|EP|OVA|OAD|SP|特别篇|特別篇|【?剧场版】?|[-_\u4e00-\u9fa5])(?<epnumber>[0-9.]+))?(?:\[[^\]]*])*(?:.*?\.bhd)?(?:\..*?)?(?:\ \(.*?)?\.[A-z1-9]+)$") { IsNamed = true }, // *** Begin Kodi Standard Naming // <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 --> new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$") { IsNamed = true }, // <!-- foo.ep01, foo.EP_01 --> new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true) { DateTimeFormats = new[] { "yyyy.MM.dd", "yyyy-MM-dd", "yyyy_MM_dd" } }, new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[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)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$") { IsNamed = true }, new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names // [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$") { IsNamed = true }, // /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, SupportsAbsoluteEpisodeNumbers = false }, new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, // *** End Kodi Standard Naming new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$") { IsNamed = true }, new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$") { IsNamed = true }, // "01.avi" new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[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(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01.blah.avi" new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[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(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01 episode title.avi" new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$") { IsOptimistic = true, IsNamed = true }, // "Episode 16", "Episode 16 - Title" new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[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_-]?(?<chapter>[0-9]+)", // Detect specified parts, like Part 02 @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)", // Chapter is often beginning of filename "^(?<chapter>[0-9]+)", // Part if often ending of filename @"(?<!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>[0-9]{4})\s*\)\s*$", @"^\s*(?<name>[^ ].*?)\s*$" }; 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]?(?<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}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][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}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$" }.Select(i => new EpisodeExpression(i) { IsNamed = true }).ToArray(); VideoFileExtensions = extensions .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); Compile(); }
private async Task <Either <BaseError, Unit> > ScanMusicVideos( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, Artist artist, string artistFolder, CancellationToken cancellationToken) { var folderQueue = new Queue <string>(); folderQueue.Enqueue(artistFolder); while (folderQueue.Count > 0) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } string musicVideoFolder = folderQueue.Dequeue(); // _logger.LogDebug("Scanning music video folder {Folder}", musicVideoFolder); var allFiles = _localFileSystem.ListFiles(musicVideoFolder) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => !Path.GetFileName(f).StartsWith("._")) .ToList(); foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder) .OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } string etag = FolderEtag.Calculate(musicVideoFolder, _localFileSystem); Option <LibraryFolder> knownFolder = libraryPath.LibraryFolders .Filter(f => f.Path == musicVideoFolder) .HeadOrNone(); // skip folder if etag matches if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } foreach (string file in allFiles.OrderBy(identity)) { // TODO: figure out how to rebuild playouts Either <BaseError, MediaItemScanResult <MusicVideo> > maybeMusicVideo = await _musicVideoRepository .GetOrAdd(artist, libraryPath, file) .BindT(musicVideo => UpdateStatistics(musicVideo, ffmpegPath, ffprobePath)) .BindT(UpdateMetadata) .BindT(result => UpdateThumbnail(result, cancellationToken)) .BindT(UpdateSubtitles) .BindT(FlagNormal); foreach (BaseError error in maybeMusicVideo.LeftToSeq()) { _logger.LogWarning("Error processing music video at {Path}: {Error}", file, error.Value); } foreach (MediaItemScanResult <MusicVideo> result in maybeMusicVideo.RightToSeq()) { if (result.IsAdded) { await _searchIndex.AddItems(_searchRepository, new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(_searchRepository, new List <MediaItem> { result.Item }); } await _libraryRepository.SetEtag(libraryPath, knownFolder, musicVideoFolder, etag); } } } return(Unit.Default); }
public async Task <Either <BaseError, Unit> > ScanFolder(LibraryPath libraryPath, string ffprobePath) { if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) { return(new MediaSourceInaccessible()); } var folderQueue = new Queue <string>(); foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) { folderQueue.Enqueue(folder); } while (folderQueue.Count > 0) { string movieFolder = folderQueue.Dequeue(); var allFiles = _localFileSystem.ListFiles(movieFolder) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter( f => !ExtraFiles.Any( e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase))) .ToList(); if (allFiles.Count == 0) { foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } continue; } foreach (string file in allFiles.OrderBy(identity)) { // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <Movie> > maybeMovie = await _movieRepository .GetOrAdd(libraryPath, file) .BindT(movie => UpdateStatistics(movie, ffprobePath)) .BindT(UpdateMetadata) .BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster)) .BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt)); await maybeMovie.Match( async result => { if (result.IsAdded) { await _searchIndex.AddItems(new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(new List <MediaItem> { result.Item }); } }, error => { _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value); return(Task.CompletedTask); }); } } foreach (string path in await _movieRepository.FindMoviePaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Removing missing movie at {Path}", path); List <int> ids = await _movieRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(ids); } } return(Unit.Default); }
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][0-9]|20[0-1][0-9])([ _\,\.\(\)\[\]\-][^0-9]|$)" }; CleanStrings = new[] { @"[ _\,\.\(\)\[\]\-](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|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|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"(\[.*\])" }; 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" }; // Notice that these expressions need to match the whole path without the extension EpisodeExpressions = new[] { // *** Start Kodi Standard Naming // sname.s01.e01, sname.s01_e01, S01E02 sname, S01 - E02 new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$") { IsNamed = true }, // foo.ep01, foo.EP_01 new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), // 0001-01-01, 0001 01 01 [is this useless because they do not match the paths?] new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true) { DateTimeFormats = new[] { "yyyy.MM.dd", "yyyy-MM-dd", "yyyy_MM_dd" } }, // 01-01-0001 [is this useless because they do not match the paths?] new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true) { DateTimeFormats = new[] { "dd.MM.yyyy", "dd-MM-yyyy", "dd_MM_yyyy" } }, // 01x01 foo new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, // sname0001/0000101 [last two numbers are episodes, the rest is season] {also why is it matched weirdly after the series name?} 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, SupportsAbsoluteEpisodeNumbers = false }, // what is this? are we matching the letter a and r? and afterwards some ivx? new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$") { SupportsAbsoluteEpisodeNumbers = true }, // *** End Kodi Standard Naming // S0001E001 (or less leading zeroes and lower case) new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})[^\\\/]*$") { IsNamed = true }, // S0001xE001 (or less leading zeroes and lower case) new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d{1,4})[x,X]?[eE](?<epnumber>\d{1,3})[^\\\/]*$") { IsNamed = true }, // sname S0001X001/S0001X001 new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))[^\\\/]*$") { IsNamed = true }, // foo/S0001.E001 (or x instead of .) new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})[^\\\/]*$") { IsNamed = true }, // "01" new EpisodeExpression(@".*[\\\/](?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.\w+$") { IsOptimistic = true, IsNamed = true }, // "1-12 episode title" new EpisodeExpression(@"([0-9]+)-([0-9]+)"), // "01 - foo", "01-foo.avi" new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01.foo.avi" new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$") { IsOptimistic = true, IsNamed = true }, // "foo - 01", "foo 2 - 01", "foo - 01 foo", "foo 2 - 01 foo", "foo - 01 - foo", "foo 2 - 01 - bar" new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "01 episode title.avi" new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$") { IsOptimistic = true, IsNamed = true }, // "Episode 16", "Episode 16 - Title" new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$") { IsOptimistic = true, IsNamed = true }, // "[*] Name 01 [*] new EpisodeExpression(@".*?\[.*?\].*?(?<seriesname>\S+?(\s.+?)*?)[-\s]+(?<epnumber>[0-9]+).*\[.*?\]$") { 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", RuleType = ExtraRuleType.Filename, Token = "trailer", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "trailer", RuleType = ExtraRuleType.Suffix, Token = "-trailer", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "trailer", RuleType = ExtraRuleType.Suffix, Token = ".trailer", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "trailer", RuleType = ExtraRuleType.Suffix, Token = "_trailer", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "trailer", RuleType = ExtraRuleType.Suffix, Token = " trailer", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "sample", RuleType = ExtraRuleType.Filename, Token = "sample", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "sample", RuleType = ExtraRuleType.Suffix, Token = "-sample", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "sample", RuleType = ExtraRuleType.Suffix, Token = ".sample", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "sample", RuleType = ExtraRuleType.Suffix, Token = "_sample", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "sample", RuleType = ExtraRuleType.Suffix, Token = " sample", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "themesong", RuleType = ExtraRuleType.Filename, Token = "theme", MediaType = MediaType.Audio }, new ExtraRule { ExtraType = "scene", RuleType = ExtraRuleType.Suffix, Token = "-scene", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "clip", RuleType = ExtraRuleType.Suffix, Token = "-clip", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "interview", RuleType = ExtraRuleType.Suffix, Token = "-interview", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "behindthescenes", RuleType = ExtraRuleType.Suffix, Token = "-behindthescenes", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "deletedscene", RuleType = ExtraRuleType.Suffix, Token = "-deleted", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "featurette", RuleType = ExtraRuleType.Suffix, Token = "-featurette", MediaType = MediaType.Video }, new ExtraRule { ExtraType = "short", RuleType = ExtraRuleType.Suffix, Token = "-short", MediaType = 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" } }; AudioBookPartsExpressions = new[] { // Detect specified chapters, like CH 01 @"ch(?:apter)?[\s_-]?(?<chapter>\d+)", // Detect specified parts, like Part 02 @"p(?:ar)?t[\s_-]?(?<part>\d+)", // Chapter is often beginning of filename @"^(?<chapter>\d+)", // Part if often ending of filename @"(?<part>\d+)$", // Sometimes named as 0001_005 (chapter_part) @"(?<chapter>\d+)_(?<part>\d+)", // Some audiobooks are ripped from cd's, and will be named by disk number. @"dis(?:c|k)[\s_-]?(?<chapter>\d+)" }; 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 string[] { @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$" }.Select(i => new EpisodeExpression(i) { IsNamed = true }).ToArray(); VideoFileExtensions = extensions .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); Compile(); }
public async Task <Either <BaseError, Unit> > ScanFolder( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, decimal progressMin, decimal progressMax, CancellationToken cancellationToken) { try { decimal progressSpread = progressMax - progressMin; var foldersCompleted = 0; var folderQueue = new Queue <string>(); if (ShouldIncludeFolder(libraryPath.Path)) { folderQueue.Enqueue(libraryPath.Path); } foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path) .Filter(ShouldIncludeFolder) .OrderBy(identity)) { folderQueue.Enqueue(folder); } while (folderQueue.Count > 0) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); await _mediator.Publish( new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion *progressSpread), cancellationToken); string otherVideoFolder = folderQueue.Dequeue(); foldersCompleted++; var filesForEtag = _localFileSystem.ListFiles(otherVideoFolder).ToList(); var allFiles = filesForEtag .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => !Path.GetFileName(f).StartsWith("._")) .ToList(); foreach (string subdirectory in _localFileSystem.ListSubdirectories(otherVideoFolder) .Filter(ShouldIncludeFolder) .OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } string etag = FolderEtag.Calculate(otherVideoFolder, _localFileSystem); Option <LibraryFolder> knownFolder = libraryPath.LibraryFolders .Filter(f => f.Path == otherVideoFolder) .HeadOrNone(); // skip folder if etag matches if (!allFiles.Any() || await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } _logger.LogDebug( "UPDATE: Etag has changed for folder {Folder}", otherVideoFolder); foreach (string file in allFiles.OrderBy(identity)) { Either <BaseError, MediaItemScanResult <OtherVideo> > maybeVideo = await _otherVideoRepository .GetOrAdd(libraryPath, file) .BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath)) .BindT(UpdateMetadata) .BindT(UpdateSubtitles) .BindT(FlagNormal); foreach (BaseError error in maybeVideo.LeftToSeq()) { _logger.LogWarning("Error processing other video at {Path}: {Error}", file, error.Value); } foreach (MediaItemScanResult <OtherVideo> result in maybeVideo.RightToSeq()) { if (result.IsAdded) { await _searchIndex.AddItems(_searchRepository, new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(_searchRepository, new List <MediaItem> { result.Item }); } await _libraryRepository.SetEtag(libraryPath, knownFolder, otherVideoFolder, etag); } } } foreach (string path in await _otherVideoRepository.FindOtherVideoPaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Flagging missing other video at {Path}", path); List <int> otherVideoIds = await FlagFileNotFound(libraryPath, path); await _searchIndex.RebuildItems(_searchRepository, otherVideoIds); } else if (Path.GetFileName(path).StartsWith("._")) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List <int> otherVideoIds = await _otherVideoRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(otherVideoIds); } } await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); return(Unit.Default); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { _searchIndex.Commit(); } }