/// <summary> /// Resolves alternative versions and extras from list of video files. /// </summary> /// <param name="files">List of related video files.</param> /// <param name="namingOptions">The naming options.</param> /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param> /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns> public static IEnumerable <VideoInfo> Resolve(IEnumerable <FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true) { var videoInfos = files .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions)) .OfType <VideoFileInfo>() .ToList(); // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer var nonExtras = videoInfos .Where(i => i.ExtraType == null) .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); var stackResult = new StackResolver(namingOptions) .Resolve(nonExtras).ToList(); var remainingFiles = videoInfos .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory))) .ToList(); var list = new List <VideoInfo>(); foreach (var stack in stackResult) { var info = new VideoInfo(stack.Name) { Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions)) .OfType <VideoFileInfo>() .ToList() }; info.Year = info.Files[0].Year; var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters); if (extras.Count > 0) { info.Extras = extras; } list.Add(info); } var standaloneMedia = remainingFiles .Where(i => i.ExtraType == null) .ToList(); foreach (var media in standaloneMedia) { var info = new VideoInfo(media.Name) { Files = new[] { media } }; info.Year = info.Files[0].Year; remainingFiles.Remove(media); var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters); info.Extras = extras; list.Add(info); } if (supportMultiVersion) { list = GetVideosGroupedByVersion(list, namingOptions); } // If there's only one resolved video, use the folder name as well to find extras if (list.Count == 1) { var info = list[0]; var videoPath = list[0].Files[0].Path; var parentPath = Path.GetDirectoryName(videoPath.AsSpan()); if (!parentPath.IsEmpty) { var folderName = Path.GetFileName(parentPath); if (!folderName.IsEmpty) { var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters); extras.AddRange(info.Extras); info.Extras = extras; } } // Add the extras that are just based on file name as well var extrasByFileName = remainingFiles .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename) .ToList(); remainingFiles = remainingFiles .Except(extrasByFileName) .ToList(); extrasByFileName.AddRange(info.Extras); info.Extras = extrasByFileName; } // If there's only one video, accept all trailers // Be lenient because people use all kinds of mishmash conventions with trailers. if (list.Count == 1) { var trailers = remainingFiles .Where(i => i.ExtraType == ExtraType.Trailer) .ToList(); trailers.AddRange(list[0].Extras); list[0].Extras = trailers; remainingFiles = remainingFiles .Except(trailers) .ToList(); } // Whatever files are left, just add them list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name) { Files = new[] { i }, Year = i.Year })); return(list); }
/// <summary> /// Resolves alternative versions and extras from list of video files. /// </summary> /// <param name="files">List of related video files.</param> /// <param name="namingOptions">The naming options.</param> /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param> /// <param name="parseName">Whether to parse the name or use the filename.</param> /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns> public static IReadOnlyList <VideoInfo> Resolve(IEnumerable <FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true) { var videoInfos = files .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions, parseName)) .OfType <VideoFileInfo>() .ToList(); // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer var nonExtras = videoInfos .Where(i => i.ExtraType == null) .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList(); var remainingFiles = new List <VideoFileInfo>(); var standaloneMedia = new List <VideoFileInfo>(); for (var i = 0; i < videoInfos.Count; i++) { var current = videoInfos[i]; if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory))) { continue; } remainingFiles.Add(current); if (current.ExtraType == null) { standaloneMedia.Add(current); } } var list = new List <VideoInfo>(); foreach (var stack in stackResult) { var info = new VideoInfo(stack.Name) { Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName)) .OfType <VideoFileInfo>() .ToList() }; info.Year = info.Files[0].Year; list.Add(info); } foreach (var media in standaloneMedia) { var info = new VideoInfo(media.Name) { Files = new[] { media } }; info.Year = info.Files[0].Year; remainingFiles.Remove(media); list.Add(info); } if (supportMultiVersion) { list = GetVideosGroupedByVersion(list, namingOptions); } // Whatever files are left, just add them list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name) { Files = new[] { i }, Year = i.Year, ExtraType = i.ExtraType })); return(list); }
public IEnumerable <VideoInfo> Resolve(List <FileSystemMetadata> files, bool supportMultiVersion = true) { var videoResolver = new VideoResolver(_options, _regexProvider); var videoInfos = files .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory)) .Where(i => i != null) .ToList(); // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer var nonExtras = videoInfos .Where(i => string.IsNullOrEmpty(i.ExtraType)) .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); var stackResult = new StackResolver(_options, _regexProvider) .Resolve(nonExtras); var remainingFiles = videoInfos .Where(i => !stackResult.Stacks.Any(s => s.ContainsFile(i.Path, i.IsDirectory))) .ToList(); var list = new List <VideoInfo>(); foreach (var stack in stackResult.Stacks) { var info = new VideoInfo { Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList(), Name = stack.Name }; info.Year = info.Files.First().Year; var extraBaseNames = new List <string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) }; var extras = GetExtras(remainingFiles, extraBaseNames); if (extras.Count > 0) { remainingFiles = remainingFiles .Except(extras) .ToList(); info.Extras = extras; } list.Add(info); } var standaloneMedia = remainingFiles .Where(i => string.IsNullOrEmpty(i.ExtraType)) .ToList(); foreach (var media in standaloneMedia) { var info = new VideoInfo { Files = new List <VideoFileInfo> { media }, Name = media.Name }; info.Year = info.Files.First().Year; var extras = GetExtras(remainingFiles, new List <string> { media.FileNameWithoutExtension }); remainingFiles = remainingFiles .Except(extras.Concat(new[] { media })) .ToList(); info.Extras = extras; list.Add(info); } if (supportMultiVersion) { list = GetVideosGroupedByVersion(list) .ToList(); } // If there's only one resolved video, use the folder name as well to find extras if (list.Count == 1) { var info = list[0]; var videoPath = list[0].Files[0].Path; var parentPath = Path.GetDirectoryName(videoPath); if (!string.IsNullOrEmpty(parentPath)) { var folderName = Path.GetFileName(Path.GetDirectoryName(videoPath)); if (!string.IsNullOrEmpty(folderName)) { var extras = GetExtras(remainingFiles, new List <string> { folderName }); remainingFiles = remainingFiles .Except(extras) .ToList(); info.Extras.AddRange(extras); } } // Add the extras that are just based on file name as well var extrasByFileName = remainingFiles .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename) .ToList(); remainingFiles = remainingFiles .Except(extrasByFileName) .ToList(); info.Extras.AddRange(extrasByFileName); } // If there's only one video, accept all trailers // Be lenient because people use all kinds of mish mash conventions with trailers if (list.Count == 1) { var trailers = remainingFiles .Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)) .ToList(); list[0].Extras.AddRange(trailers); remainingFiles = remainingFiles .Except(trailers) .ToList(); } // Whatever files are left, just add them list.AddRange(remainingFiles.Select(i => new VideoInfo { Files = new List <VideoFileInfo> { i }, Name = i.Name, Year = i.Year })); var orderedList = list.OrderBy(i => i.Name); return(orderedList); }
/// <summary> /// Attempts to resolve if file is extra. /// </summary> /// <param name="path">Path to file.</param> /// <param name="namingOptions">The naming options.</param> /// <returns>Returns <see cref="ExtraResult"/> object.</returns> public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions) { var result = new ExtraResult(); for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++) { var rule = namingOptions.VideoExtraRules[i]; if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions)) || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions))) { continue; } var pathSpan = path.AsSpan(); if (rule.RuleType == ExtraRuleType.Filename) { var filename = Path.GetFileNameWithoutExtension(pathSpan); if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) { result.ExtraType = rule.ExtraType; result.Rule = rule; } } else if (rule.RuleType == ExtraRuleType.Suffix) { // Trim the digits from the end of the filename so we can recognize things like -trailer2 var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits); if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase)) { result.ExtraType = rule.ExtraType; result.Rule = rule; } } else if (rule.RuleType == ExtraRuleType.Regex) { var filename = Path.GetFileName(path); var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled); if (isMatch) { result.ExtraType = rule.ExtraType; result.Rule = rule; } } else if (rule.RuleType == ExtraRuleType.DirectoryName) { var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan)); if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) { result.ExtraType = rule.ExtraType; result.Rule = rule; } } if (result.ExtraType != null) { return(result); } } return(result); }
public StackResult Resolve(IEnumerable <FileSystemMetadata> files) { var result = new StackResult(); var resolver = new VideoResolver(_options); var list = files .Where(i => i.IsDirectory || (resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))) .OrderBy(i => i.FullName) .ToList(); var expressions = _options.VideoFileStackingRegexes; for (var i = 0; i < list.Count; i++) { var offset = 0; var file1 = list[i]; var expressionIndex = 0; while (expressionIndex < expressions.Length) { var exp = expressions[expressionIndex]; var stack = new FileStack(); // (Title)(Volume)(Ignore)(Extension) var match1 = FindMatch(file1, exp, offset); 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 j = i + 1; while (j < list.Count) { var file2 = list[j]; if (file1.IsDirectory != file2.IsDirectory) { j++; continue; } // (Title)(Volume)(Ignore)(Extension) var match2 = FindMatch(file2, exp, offset); if (match2.Success) { var title2 = match2.Groups[1].Value; var volume2 = match2.Groups[2].Value; var ignore2 = match2.Groups[3].Value; var extension2 = match2.Groups[4].Value; if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase)) { if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase)) { if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase) && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase)) { if (stack.Files.Count == 0) { stack.Name = title1 + ignore1; stack.IsDirectoryStack = file1.IsDirectory; stack.Files.Add(file1.FullName); } stack.Files.Add(file2.FullName); } else { // Sequel offset = 0; expressionIndex++; break; } } else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)) { // False positive, try again with offset offset = match1.Groups[3].Index; break; } else { // Extension mismatch offset = 0; expressionIndex++; break; } } else { // Title mismatch offset = 0; expressionIndex++; break; } } else { // No match 2, next expression offset = 0; expressionIndex++; break; } j++; } if (j == list.Count) { expressionIndex = expressions.Length; } } else { // No match 1 offset = 0; expressionIndex++; } if (stack.Files.Count > 1) { result.Stacks.Add(stack); i += stack.Files.Count - 1; break; } } } return(result); }
/// <summary> /// Resolves the video. /// </summary> /// <typeparam name="TVideoType">The type of the T video type.</typeparam> /// <param name="args">The args.</param> /// <param name="parseName">if set to <c>true</c> [parse name].</param> /// <returns>``0.</returns> protected TVideoType ResolveVideo <TVideoType>(ItemResolveArgs args, bool parseName) where TVideoType : Video, new() { var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); // If the path is a file check for a matching extensions var parser = new Emby.Naming.Video.VideoResolver(namingOptions); if (args.IsDirectory) { TVideoType video = null; VideoFileInfo videoInfo = null; // Loop through each child file/folder and see if we find a video foreach (var child in args.FileSystemChildren) { var filename = child.Name; if (child.IsDirectory) { if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) { videoInfo = parser.ResolveDirectory(args.Path); if (videoInfo == null) { return(null); } video = new TVideoType { Path = args.Path, VideoType = VideoType.Dvd, ProductionYear = videoInfo.Year }; break; } if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService)) { videoInfo = parser.ResolveDirectory(args.Path); if (videoInfo == null) { return(null); } video = new TVideoType { Path = args.Path, VideoType = VideoType.BluRay, ProductionYear = videoInfo.Year }; break; } } else if (IsDvdFile(filename)) { videoInfo = parser.ResolveDirectory(args.Path); if (videoInfo == null) { return(null); } video = new TVideoType { Path = args.Path, VideoType = VideoType.Dvd, ProductionYear = videoInfo.Year }; break; } } if (video != null) { video.Name = parseName ? videoInfo.Name : Path.GetFileName(args.Path); Set3DFormat(video, videoInfo); } return(video); } else { var videoInfo = parser.Resolve(args.Path, false, false); if (videoInfo == null) { return(null); } if (LibraryManager.IsVideoFile(args.Path, args.GetLibraryOptions()) || videoInfo.IsStub) { var path = args.Path; var video = new TVideoType { Path = path, IsInMixedFolder = true, ProductionYear = videoInfo.Year }; SetVideoType(video, videoInfo); video.Name = parseName ? videoInfo.Name : Path.GetFileNameWithoutExtension(args.Path); Set3DFormat(video, videoInfo); return(video); } } return(null); }
/// <summary> /// Resolves videos from paths. /// </summary> /// <param name="files">List of paths.</param> /// <param name="namingOptions">The naming options.</param> /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns> public static IEnumerable <FileStack> Resolve(IEnumerable <FileSystemMetadata> files, NamingOptions namingOptions) { var potentialFiles = files .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions)) .OrderBy(i => i.FullName); var potentialStacks = new Dictionary <string, StackMetadata>(); foreach (var file in potentialFiles) { var name = file.Name; if (string.IsNullOrEmpty(name)) { name = Path.GetFileName(file.FullName); } for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++) { var rule = namingOptions.VideoFileStackingRules[i]; if (!rule.Match(name, out var stackParsingResult)) { continue; } var stackName = stackParsingResult.Value.StackName; var partNumber = stackParsingResult.Value.PartNumber; var partType = stackParsingResult.Value.PartType; if (!potentialStacks.TryGetValue(stackName, out var stackResult)) { stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType); potentialStacks[stackName] = stackResult; } if (stackResult.Parts.Count > 0) { if (stackResult.IsDirectory != file.IsDirectory || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase) || stackResult.ContainsPart(partNumber)) { continue; } if (rule.IsNumerical != stackResult.IsNumerical) { break; } } stackResult.Parts.Add(partNumber, file); break; } } foreach (var(fileName, stack) in potentialStacks) { if (stack.Parts.Count < 2) { continue; } yield return(new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray())); } }
/// <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) { var result = new ExtraResult(); for (var i = 0; i < _options.VideoExtraRules.Length; i++) { var rule = _options.VideoExtraRules[i]; if (rule.MediaType == MediaType.Audio) { if (!AudioFileParser.IsAudioFile(path, _options)) { continue; } } else if (rule.MediaType == MediaType.Video) { if (!VideoResolver.IsVideoFile(path, _options)) { continue; } } var pathSpan = path.AsSpan(); if (rule.RuleType == ExtraRuleType.Filename) { var filename = Path.GetFileNameWithoutExtension(pathSpan); if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) { result.ExtraType = rule.ExtraType; result.Rule = rule; } } else if (rule.RuleType == ExtraRuleType.Suffix) { var filename = Path.GetFileNameWithoutExtension(pathSpan); if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase)) { result.ExtraType = rule.ExtraType; result.Rule = rule; } } else if (rule.RuleType == ExtraRuleType.Regex) { var filename = Path.GetFileName(path); var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); if (regex.IsMatch(filename)) { result.ExtraType = rule.ExtraType; result.Rule = rule; } } else if (rule.RuleType == ExtraRuleType.DirectoryName) { var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan)); if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) { result.ExtraType = rule.ExtraType; result.Rule = rule; } } if (result.ExtraType != null) { return(result); } } return(result); }