diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-03-15 18:52:43 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-03-15 18:52:43 -0400 |
| commit | bf30936550a0b9be69e646a1b27988914ce9ec4a (patch) | |
| tree | 01f492d79e714ff2efff69a54a514c449716def5 | |
| parent | d7cfa0d22cad210fb37dd8aa6bcf41b416129e58 (diff) | |
#712 - Support grouping multiple versions of a movie
| -rw-r--r-- | MediaBrowser.Api/VideosService.cs | 50 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/BaseItem.cs | 77 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Folder.cs | 112 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Video.cs | 161 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs | 16 | ||||
| -rw-r--r-- | MediaBrowser.Model/Dto/BaseItemDto.cs | 1 | ||||
| -rw-r--r-- | MediaBrowser.Server.Implementations/Dto/DtoService.cs | 1 | ||||
| -rw-r--r-- | MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs | 86 | ||||
| -rw-r--r-- | MediaBrowser.Tests/Resolvers/MovieResolverTests.cs | 32 |
9 files changed, 413 insertions, 123 deletions
diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs index fb58e58b7..31fda199e 100644 --- a/MediaBrowser.Api/VideosService.cs +++ b/MediaBrowser.Api/VideosService.cs @@ -22,6 +22,21 @@ namespace MediaBrowser.Api [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } } + + [Route("/Videos/{Id}/AlternateVersions", "GET")] + [Api(Description = "Gets alternate versions of a video.")] + public class GetAlternateVersions : IReturn<ItemsResult> + { + [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid? UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } public class VideosService : BaseApiService { @@ -48,7 +63,37 @@ namespace MediaBrowser.Api var item = string.IsNullOrEmpty(request.Id) ? (request.UserId.HasValue ? user.RootFolder - : (Folder)_libraryManager.RootFolder) + : _libraryManager.RootFolder) + : _dtoService.GetItemByDtoId(request.Id, request.UserId); + + // Get everything + var fields = Enum.GetNames(typeof(ItemFields)) + .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) + .ToList(); + + var video = (Video)item; + + var items = video.GetAdditionalParts() + .Select(i => _dtoService.GetBaseItemDto(i, fields, user, video)) + .ToArray(); + + var result = new ItemsResult + { + Items = items, + TotalRecordCount = items.Length + }; + + return ToOptimizedSerializedResultUsingCache(result); + } + + public object Get(GetAlternateVersions request) + { + var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null; + + var item = string.IsNullOrEmpty(request.Id) + ? (request.UserId.HasValue + ? user.RootFolder + : _libraryManager.RootFolder) : _dtoService.GetItemByDtoId(request.Id, request.UserId); // Get everything @@ -58,8 +103,7 @@ namespace MediaBrowser.Api var video = (Video)item; - var items = video.AdditionalPartIds.Select(_libraryManager.GetItemById) - .OrderBy(i => i.SortName) + var items = video.GetAlternateVersions() .Select(i => _dtoService.GetBaseItemDto(i, fields, user, video)) .ToArray(); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index e0c792307..be64d20c3 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -955,6 +955,83 @@ namespace MediaBrowser.Controller.Entities } /// <summary> + /// Gets the linked child. + /// </summary> + /// <param name="info">The info.</param> + /// <returns>BaseItem.</returns> + protected BaseItem GetLinkedChild(LinkedChild info) + { + // First get using the cached Id + if (info.ItemId.HasValue) + { + if (info.ItemId.Value == Guid.Empty) + { + return null; + } + + var itemById = LibraryManager.GetItemById(info.ItemId.Value); + + if (itemById != null) + { + return itemById; + } + } + + var item = FindLinkedChild(info); + + // If still null, log + if (item == null) + { + // Don't keep searching over and over + info.ItemId = Guid.Empty; + } + else + { + // Cache the id for next time + info.ItemId = item.Id; + } + + return item; + } + + private BaseItem FindLinkedChild(LinkedChild info) + { + if (!string.IsNullOrEmpty(info.Path)) + { + var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path); + + if (itemByPath == null) + { + Logger.Warn("Unable to find linked item at path {0}", info.Path); + } + + return itemByPath; + } + + if (!string.IsNullOrWhiteSpace(info.ItemName) && !string.IsNullOrWhiteSpace(info.ItemType)) + { + return LibraryManager.RootFolder.RecursiveChildren.FirstOrDefault(i => + { + if (string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(i.GetType().Name, info.ItemType, StringComparison.OrdinalIgnoreCase)) + { + if (info.ItemYear.HasValue) + { + return info.ItemYear.Value == (i.ProductionYear ?? -1); + } + return true; + } + } + + return false; + }); + } + + return null; + } + + /// <summary> /// Adds a person to the item /// </summary> /// <param name="person">The person.</param> diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index ee371680e..45daaba0b 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -354,20 +354,45 @@ namespace MediaBrowser.Controller.Entities private bool IsValidFromResolver(BaseItem current, BaseItem newItem) { - var currentAsPlaceHolder = current as ISupportsPlaceHolders; + var currentAsVideo = current as Video; - if (currentAsPlaceHolder != null) + if (currentAsVideo != null) { - var newHasPlaceHolder = newItem as ISupportsPlaceHolders; + var newAsVideo = newItem as Video; - if (newHasPlaceHolder != null) + if (newAsVideo != null) { - if (currentAsPlaceHolder.IsPlaceHolder != newHasPlaceHolder.IsPlaceHolder) + if (currentAsVideo.IsPlaceHolder != newAsVideo.IsPlaceHolder) + { + return false; + } + if (currentAsVideo.IsMultiPart != newAsVideo.IsMultiPart) + { + return false; + } + if (currentAsVideo.HasLocalAlternateVersions != newAsVideo.HasLocalAlternateVersions) { return false; } } } + else + { + var currentAsPlaceHolder = current as ISupportsPlaceHolders; + + if (currentAsPlaceHolder != null) + { + var newHasPlaceHolder = newItem as ISupportsPlaceHolders; + + if (newHasPlaceHolder != null) + { + if (currentAsPlaceHolder.IsPlaceHolder != newHasPlaceHolder.IsPlaceHolder) + { + return false; + } + } + } + } return current.IsInMixedFolder == newItem.IsInMixedFolder; } @@ -898,83 +923,6 @@ namespace MediaBrowser.Controller.Entities .Where(i => i != null); } - /// <summary> - /// Gets the linked child. - /// </summary> - /// <param name="info">The info.</param> - /// <returns>BaseItem.</returns> - private BaseItem GetLinkedChild(LinkedChild info) - { - // First get using the cached Id - if (info.ItemId.HasValue) - { - if (info.ItemId.Value == Guid.Empty) - { - return null; - } - - var itemById = LibraryManager.GetItemById(info.ItemId.Value); - - if (itemById != null) - { - return itemById; - } - } - - var item = FindLinkedChild(info); - - // If still null, log - if (item == null) - { - // Don't keep searching over and over - info.ItemId = Guid.Empty; - } - else - { - // Cache the id for next time - info.ItemId = item.Id; - } - - return item; - } - - private BaseItem FindLinkedChild(LinkedChild info) - { - if (!string.IsNullOrEmpty(info.Path)) - { - var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path); - - if (itemByPath == null) - { - Logger.Warn("Unable to find linked item at path {0}", info.Path); - } - - return itemByPath; - } - - if (!string.IsNullOrWhiteSpace(info.ItemName) && !string.IsNullOrWhiteSpace(info.ItemType)) - { - return LibraryManager.RootFolder.RecursiveChildren.FirstOrDefault(i => - { - if (string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(i.GetType().Name, info.ItemType, StringComparison.OrdinalIgnoreCase)) - { - if (info.ItemYear.HasValue) - { - return info.ItemYear.Value == (i.ProductionYear ?? -1); - } - return true; - } - } - - return false; - }); - } - - return null; - } - protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) { var changesFound = false; diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 10034d7e5..e30458dd8 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -19,15 +19,63 @@ namespace MediaBrowser.Controller.Entities public class Video : BaseItem, IHasMediaStreams, IHasAspectRatio, IHasTags, ISupportsPlaceHolders { public bool IsMultiPart { get; set; } + public bool HasLocalAlternateVersions { get; set; } public List<Guid> AdditionalPartIds { get; set; } + public List<Guid> AlternateVersionIds { get; set; } public Video() { PlayableStreamFileNames = new List<string>(); AdditionalPartIds = new List<Guid>(); + AlternateVersionIds = new List<Guid>(); Tags = new List<string>(); SubtitleFiles = new List<string>(); + LinkedAlternateVersions = new List<LinkedChild>(); + } + + [IgnoreDataMember] + public bool HasAlternateVersions + { + get + { + return HasLocalAlternateVersions || LinkedAlternateVersions.Count > 0; + } + } + + public List<LinkedChild> LinkedAlternateVersions { get; set; } + + /// <summary> + /// Gets the linked children. + /// </summary> + /// <returns>IEnumerable{BaseItem}.</returns> + public IEnumerable<BaseItem> GetAlternateVersions() + { + var filesWithinSameDirectory = AlternateVersionIds + .Select(i => LibraryManager.GetItemById(i)) + .Where(i => i != null) + .OfType<Video>(); + + var linkedVersions = LinkedAlternateVersions + .Select(GetLinkedChild) + .Where(i => i != null) + .OfType<Video>(); + + return filesWithinSameDirectory.Concat(linkedVersions) + .OrderBy(i => i.SortName); + } + + /// <summary> + /// Gets the additional parts. + /// </summary> + /// <returns>IEnumerable{Video}.</returns> + public IEnumerable<Video> GetAdditionalParts() + { + return AdditionalPartIds + .Select(i => LibraryManager.GetItemById(i)) + .Where(i => i != null) + .OfType<Video>() + .OrderBy(i => i.SortName); } /// <summary> @@ -43,13 +91,13 @@ namespace MediaBrowser.Controller.Entities public bool HasSubtitles { get; set; } public bool IsPlaceHolder { get; set; } - + /// <summary> /// Gets or sets the tags. /// </summary> /// <value>The tags.</value> public List<string> Tags { get; set; } - + /// <summary> /// Gets or sets the video bit rate. /// </summary> @@ -167,22 +215,53 @@ namespace MediaBrowser.Controller.Entities { var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); - // Must have a parent to have additional parts + // Must have a parent to have additional parts or alternate versions // In other words, it must be part of the Parent/Child tree // The additional parts won't have additional parts themselves - if (IsMultiPart && LocationType == LocationType.FileSystem && Parent != null) + if (LocationType == LocationType.FileSystem && Parent != null) { - var additionalPartsChanged = await RefreshAdditionalParts(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + if (IsMultiPart) + { + var additionalPartsChanged = await RefreshAdditionalParts(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); - if (additionalPartsChanged) + if (additionalPartsChanged) + { + hasChanges = true; + } + } + else { - hasChanges = true; + RefreshLinkedAlternateVersions(); + + if (HasLocalAlternateVersions) + { + var additionalPartsChanged = await RefreshAlternateVersionsWithinSameDirectory(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); + + if (additionalPartsChanged) + { + hasChanges = true; + } + } } } return hasChanges; } + private bool RefreshLinkedAlternateVersions() + { + foreach (var child in LinkedAlternateVersions) + { + // Reset the cached value + if (child.ItemId.HasValue && child.ItemId.Value == Guid.Empty) + { + child.ItemId = null; + } + } + + return false; + } + /// <summary> /// Refreshes the additional parts. /// </summary> @@ -223,7 +302,7 @@ namespace MediaBrowser.Controller.Entities { if ((i.Attributes & FileAttributes.Directory) == FileAttributes.Directory) { - return !string.Equals(i.FullName, path, StringComparison.OrdinalIgnoreCase) && EntityResolutionHelper.IsVideoFile(i.FullName) && EntityResolutionHelper.IsMultiPartFile(i.Name); + return !string.Equals(i.FullName, path, StringComparison.OrdinalIgnoreCase) && EntityResolutionHelper.IsMultiPartFolder(i.FullName) && EntityResolutionHelper.IsMultiPartFile(i.Name); } return false; @@ -258,6 +337,72 @@ namespace MediaBrowser.Controller.Entities }).OrderBy(i => i.Path).ToList(); } + private async Task<bool> RefreshAlternateVersionsWithinSameDirectory(MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken) + { + var newItems = LoadAlternateVersionsWithinSameDirectory(fileSystemChildren, options.DirectoryService).ToList(); + + var newItemIds = newItems.Select(i => i.Id).ToList(); + + var itemsChanged = !AlternateVersionIds.SequenceEqual(newItemIds); + + var tasks = newItems.Select(i => i.RefreshMetadata(options, cancellationToken)); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + AlternateVersionIds = newItemIds; + + return itemsChanged; + } + + /// <summary> + /// Loads the additional parts. + /// </summary> + /// <returns>IEnumerable{Video}.</returns> + private IEnumerable<Video> LoadAlternateVersionsWithinSameDirectory(IEnumerable<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService) + { + IEnumerable<FileSystemInfo> files; + + var path = Path; + var currentFilename = System.IO.Path.GetFileNameWithoutExtension(path) ?? string.Empty; + + // Only support this for video files. For folder rips, they'll have to use the linking feature + if (VideoType == VideoType.VideoFile || VideoType == VideoType.Iso) + { + files = fileSystemChildren.Where(i => + { + if ((i.Attributes & FileAttributes.Directory) == FileAttributes.Directory) + { + return false; + } + + return !string.Equals(i.FullName, path, StringComparison.OrdinalIgnoreCase) && + EntityResolutionHelper.IsVideoFile(i.FullName) && + i.Name.StartsWith(currentFilename, StringComparison.OrdinalIgnoreCase); + }); + } + else + { + files = new List<FileSystemInfo>(); + } + + return LibraryManager.ResolvePaths<Video>(files, directoryService, null).Select(video => + { + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(video.Id) as Video; + + if (dbItem != null) + { + video = dbItem; + } + + video.ImageInfos = ImageInfos; + + return video; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); + } + public override IEnumerable<string> GetDeletePaths() { if (!IsInMixedFolder) diff --git a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs index 0f93e8e8a..9c757503c 100644 --- a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs +++ b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs @@ -71,7 +71,21 @@ namespace MediaBrowser.Controller.Resolvers throw new ArgumentNullException("path"); } - return MultiFileRegex.Match(path).Success || MultiFolderRegex.Match(path).Success; + path = Path.GetFileName(path); + + return MultiFileRegex.Match(path).Success; + } + + public static bool IsMultiPartFolder(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + path = Path.GetFileName(path); + + return MultiFolderRegex.Match(path).Success; } /// <summary> diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 9e57f045a..d571c233b 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -494,6 +494,7 @@ namespace MediaBrowser.Model.Dto /// </summary> /// <value>The part count.</value> public int? PartCount { get; set; } + public bool? HasAlternateVersions { get; set; } /// <summary> /// Determines whether the specified type is type. diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index fadf4c900..6f6a3f043 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -1082,6 +1082,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.IsHD = video.IsHD; dto.PartCount = video.AdditionalPartIds.Count + 1; + dto.HasAlternateVersions = video.HasAlternateVersions; if (fields.Contains(ItemFields.Chapters)) { diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 16c0d1a27..de10e669e 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using MediaBrowser.Model.Logging; namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies { @@ -20,11 +21,13 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies { private readonly IServerApplicationPaths _applicationPaths; private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; - public MovieResolver(IServerApplicationPaths appPaths, ILibraryManager libraryManager) + public MovieResolver(IServerApplicationPaths appPaths, ILibraryManager libraryManager, ILogger logger) { _applicationPaths = appPaths; _libraryManager = libraryManager; + _logger = logger; } /// <summary> @@ -76,29 +79,29 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies { if (string.Equals(collectionType, CollectionType.Trailers, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Trailer>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, false); + return FindMovie<Trailer>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, false, false); } if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<MusicVideo>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, false); + return FindMovie<MusicVideo>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, false, false); } if (string.Equals(collectionType, CollectionType.AdultVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<AdultVideo>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, true); + return FindMovie<AdultVideo>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, true, false); } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Video>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, true); + return FindMovie<Video>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, true, false); } - + if (string.IsNullOrEmpty(collectionType) || string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase) || string.Equals(collectionType, CollectionType.BoxSets, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Movie>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, true); + return FindMovie<Movie>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService, true, true); } return null; @@ -187,7 +190,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies /// <param name="directoryService">The directory service.</param> /// <param name="supportMultiFileItems">if set to <c>true</c> [support multi file items].</param> /// <returns>Movie.</returns> - private T FindMovie<T>(string path, Folder parent, IEnumerable<FileSystemInfo> fileSystemEntries, IDirectoryService directoryService, bool supportMultiFileItems) + private T FindMovie<T>(string path, Folder parent, IEnumerable<FileSystemInfo> fileSystemEntries, IDirectoryService directoryService, bool supportMultiFileItems, bool supportsAlternateVersions) where T : Video, new() { var movies = new List<T>(); @@ -218,7 +221,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies }; } - if (EntityResolutionHelper.IsMultiPartFile(filename)) + if (EntityResolutionHelper.IsMultiPartFolder(filename)) { multiDiscFolders.Add(child); } @@ -248,9 +251,27 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies } } - if (movies.Count > 1 && supportMultiFileItems) + if (movies.Count > 1) { - return GetMultiFileMovie(movies); + if (supportMultiFileItems) + { + var result = GetMultiFileMovie(movies); + + if (result != null) + { + return result; + } + } + if (supportsAlternateVersions) + { + var result = GetMovieWithAlternateVersions(movies); + + if (result != null) + { + return result; + } + } + return null; } if (movies.Count == 1) @@ -356,12 +377,47 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies var firstMovie = sortedMovies[0]; // They must all be part of the sequence if we're going to consider it a multi-part movie - // Only support up to 8 (matches Plex), to help avoid incorrect detection - if (sortedMovies.All(i => EntityResolutionHelper.IsMultiPartFile(i.Path)) && sortedMovies.Count <= 8) + if (sortedMovies.All(i => EntityResolutionHelper.IsMultiPartFile(i.Path))) { - firstMovie.IsMultiPart = true; + // Only support up to 8 (matches Plex), to help avoid incorrect detection + if (sortedMovies.Count <= 8) + { + firstMovie.IsMultiPart = true; + + _logger.Info("Multi-part video found: " + firstMovie.Path); - return firstMovie; + return firstMovie; + } + } + + return null; + } + + private T GetMovieWithAlternateVersions<T>(IEnumerable<T> movies) + where T : Video, new() + { + var sortedMovies = movies.OrderBy(i => i.Path.Length).ToList(); + + // Cap this at five to help avoid incorrect matching + if (sortedMovies.Count > 5) + { + return null; + } + + var firstMovie = sortedMovies[0]; + + var filenamePrefix = Path.GetFileNameWithoutExtension(firstMovie.Path); + + if (!string.IsNullOrWhiteSpace(filenamePrefix)) + { + if (sortedMovies.All(i => Path.GetFileNameWithoutExtension(i.Path).StartsWith(filenamePrefix, StringComparison.OrdinalIgnoreCase))) + { + firstMovie.HasLocalAlternateVersions = true; + + _logger.Info("Multi-version video found: " + firstMovie.Path); + + return firstMovie; + } } return null; diff --git a/MediaBrowser.Tests/Resolvers/MovieResolverTests.cs b/MediaBrowser.Tests/Resolvers/MovieResolverTests.cs index 1cd481faa..768e4ee5c 100644 --- a/MediaBrowser.Tests/Resolvers/MovieResolverTests.cs +++ b/MediaBrowser.Tests/Resolvers/MovieResolverTests.cs @@ -9,6 +9,10 @@ namespace MediaBrowser.Tests.Resolvers [TestMethod] public void TestMultiPartFiles() { + Assert.IsFalse(EntityResolutionHelper.IsMultiPartFile(@"Braveheart.mkv")); + Assert.IsFalse(EntityResolutionHelper.IsMultiPartFile(@"Braveheart - 480p.mkv")); + Assert.IsFalse(EntityResolutionHelper.IsMultiPartFile(@"Braveheart - 720p.mkv")); + Assert.IsFalse(EntityResolutionHelper.IsMultiPartFile(@"blah blah.mkv")); Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - cd1.mkv")); @@ -33,25 +37,25 @@ namespace MediaBrowser.Tests.Resolvers [TestMethod] public void TestMultiPartFolders() { - Assert.IsFalse(EntityResolutionHelper.IsMultiPartFile(@"blah blah")); + Assert.IsFalse(EntityResolutionHelper.IsMultiPartFolder(@"blah blah")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - cd1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - disc1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - disk1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - pt1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - part1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - dvd1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - cd1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - disc1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - disk1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - pt1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - part1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - dvd1")); // Add a space - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - cd 1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - disc 1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - disk 1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - pt 1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - part 1")); - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - dvd 1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - cd 1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - disc 1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - disk 1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - pt 1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - part 1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - dvd 1")); // Not case sensitive - Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - Disc1")); + Assert.IsTrue(EntityResolutionHelper.IsMultiPartFolder(@"blah blah - Disc1")); } } } |
