Browse Source

#712 - Support grouping multiple versions of a movie

Luke Pulverenti 11 years ago
parent
commit
b36aea4ff7

+ 92 - 3
MediaBrowser.Api/VideosService.cs

@@ -1,4 +1,7 @@
-using MediaBrowser.Controller.Dto;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Querying;
@@ -37,14 +40,44 @@ 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", "POST")]
+    [Api(Description = "Assigns videos as alternates of antoher.")]
+    public class PostAlternateVersions : IReturnVoid
+    {
+        [ApiMember(Name = "AlternateVersionIds", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string AlternateVersionIds { 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; }
+    }
+
+    [Route("/Videos/{Id}/AlternateVersions", "DELETE")]
+    [Api(Description = "Assigns videos as alternates of antoher.")]
+    public class DeleteAlternateVersions : IReturnVoid
+    {
+        [ApiMember(Name = "AlternateVersionIds", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
+        public string AlternateVersionIds { 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
     {
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IDtoService _dtoService;
 
-        public VideosService( ILibraryManager libraryManager, IUserManager userManager, IDtoService dtoService)
+        public VideosService(ILibraryManager libraryManager, IUserManager userManager, IDtoService dtoService)
         {
             _libraryManager = libraryManager;
             _userManager = userManager;
@@ -115,5 +148,61 @@ namespace MediaBrowser.Api
 
             return ToOptimizedSerializedResultUsingCache(result);
         }
+
+        public void Post(PostAlternateVersions request)
+        {
+            var task = AddAlternateVersions(request);
+
+            Task.WaitAll(task);
+        }
+
+        public void Delete(DeleteAlternateVersions request)
+        {
+            var task = RemoveAlternateVersions(request);
+
+            Task.WaitAll(task);
+        }
+
+        private async Task AddAlternateVersions(PostAlternateVersions request)
+        {
+            var video = (Video)_dtoService.GetItemByDtoId(request.Id);
+
+            var list = new List<LinkedChild>();
+            var currentAlternateVersions = video.GetAlternateVersions().ToList();
+
+            foreach (var itemId in request.AlternateVersionIds.Split(',').Select(i => new Guid(i)))
+            {
+                var item = _libraryManager.GetItemById(itemId) as Video;
+
+                if (item == null)
+                {
+                    throw new ArgumentException("No item exists with the supplied Id");
+                }
+
+                if (currentAlternateVersions.Any(i => i.Id == itemId))
+                {
+                    throw new ArgumentException("Item already exists.");
+                }
+
+                list.Add(new LinkedChild
+                {
+                    Path = item.Path,
+                    Type = LinkedChildType.Manual
+                });
+
+                item.PrimaryVersionId = video.Id;
+            }
+
+            video.LinkedAlternateVersions.AddRange(list);
+
+            await video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+
+            await video.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+        }
+
+        private async Task RemoveAlternateVersions(DeleteAlternateVersions request)
+        {
+            var video = (Video)_dtoService.GetItemByDtoId(request.Id);
+        }
     }
 }

+ 68 - 18
MediaBrowser.Controller/Entities/Video.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
@@ -20,26 +21,27 @@ namespace MediaBrowser.Controller.Entities
     {
         public bool IsMultiPart { get; set; }
         public bool HasLocalAlternateVersions { get; set; }
+        public Guid? PrimaryVersionId { get; set; }
 
         public List<Guid> AdditionalPartIds { get; set; }
-        public List<Guid> AlternateVersionIds { get; set; }
+        public List<Guid> LocalAlternateVersionIds { get; set; }
 
         public Video()
         {
             PlayableStreamFileNames = new List<string>();
             AdditionalPartIds = new List<Guid>();
-            AlternateVersionIds = new List<Guid>();
+            LocalAlternateVersionIds = new List<Guid>();
             Tags = new List<string>();
             SubtitleFiles = new List<string>();
             LinkedAlternateVersions = new List<LinkedChild>();
         }
 
         [IgnoreDataMember]
-        public bool HasAlternateVersions
+        public int AlternateVersionCount
         {
             get
             {
-                return HasLocalAlternateVersions || LinkedAlternateVersions.Count > 0;
+                return LinkedAlternateVersions.Count + LocalAlternateVersionIds.Count;
             }
         }
 
@@ -51,7 +53,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>IEnumerable{BaseItem}.</returns>
         public IEnumerable<BaseItem> GetAlternateVersions()
         {
-            var filesWithinSameDirectory = AlternateVersionIds
+            var filesWithinSameDirectory = LocalAlternateVersionIds
                 .Select(i => LibraryManager.GetItemById(i))
                 .Where(i => i != null)
                 .OfType<Video>();
@@ -233,14 +235,11 @@ namespace MediaBrowser.Controller.Entities
                 {
                     RefreshLinkedAlternateVersions();
 
-                    if (HasLocalAlternateVersions)
-                    {
-                        var additionalPartsChanged = await RefreshAlternateVersionsWithinSameDirectory(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+                    var additionalPartsChanged = await RefreshAlternateVersionsWithinSameDirectory(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
 
-                        if (additionalPartsChanged)
-                        {
-                            hasChanges = true;
-                        }
+                    if (additionalPartsChanged)
+                    {
+                        hasChanges = true;
                     }
                 }
             }
@@ -339,21 +338,72 @@ namespace MediaBrowser.Controller.Entities
 
         private async Task<bool> RefreshAlternateVersionsWithinSameDirectory(MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
         {
-            var newItems = LoadAlternateVersionsWithinSameDirectory(fileSystemChildren, options.DirectoryService).ToList();
+            var newItems = HasLocalAlternateVersions ?
+                LoadAlternateVersionsWithinSameDirectory(fileSystemChildren, options.DirectoryService).ToList() :
+                new List<Video>();
 
             var newItemIds = newItems.Select(i => i.Id).ToList();
 
-            var itemsChanged = !AlternateVersionIds.SequenceEqual(newItemIds);
+            var itemsChanged = !LocalAlternateVersionIds.SequenceEqual(newItemIds);
 
-            var tasks = newItems.Select(i => i.RefreshMetadata(options, cancellationToken));
+            var tasks = newItems.Select(i => RefreshAlternateVersion(options, i, cancellationToken));
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
 
-            AlternateVersionIds = newItemIds;
+            LocalAlternateVersionIds = newItemIds;
 
             return itemsChanged;
         }
 
+        private Task RefreshAlternateVersion(MetadataRefreshOptions options, Video video, CancellationToken cancellationToken)
+        {
+            var currentImagePath = video.GetImagePath(ImageType.Primary);
+            var ownerImagePath = this.GetImagePath(ImageType.Primary);
+
+            var newOptions = new MetadataRefreshOptions
+            {
+                DirectoryService = options.DirectoryService,
+                ImageRefreshMode = options.ImageRefreshMode,
+                MetadataRefreshMode = options.MetadataRefreshMode,
+                ReplaceAllMetadata = options.ReplaceAllMetadata
+            };
+
+            if (!string.Equals(currentImagePath, ownerImagePath, StringComparison.OrdinalIgnoreCase))
+            {
+                newOptions.ForceSave = true;
+
+                if (string.IsNullOrWhiteSpace(ownerImagePath))
+                {
+                    video.ImageInfos.Clear();
+                }
+                else
+                {
+                    video.SetImagePath(ImageType.Primary, ownerImagePath);
+                }
+            }
+
+            return video.RefreshMetadata(newOptions, cancellationToken);
+        }
+
+        public override async Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
+        {
+            await base.UpdateToRepository(updateReason, cancellationToken).ConfigureAwait(false);
+
+            foreach (var item in LocalAlternateVersionIds.Select(i => LibraryManager.GetItemById(i)))
+            {
+                item.ImageInfos = ImageInfos;
+                item.Overview = Overview;
+                item.ProductionYear = ProductionYear;
+                item.PremiereDate = PremiereDate;
+                item.CommunityRating = CommunityRating;
+                item.OfficialRating = OfficialRating;
+                item.Genres = Genres;
+                item.ProviderIds = ProviderIds;
+
+                await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
         /// <summary>
         /// Loads the additional parts.
         /// </summary>
@@ -395,7 +445,7 @@ namespace MediaBrowser.Controller.Entities
                     video = dbItem;
                 }
 
-                video.ImageInfos = ImageInfos;
+                video.PrimaryVersionId = Id;
 
                 return video;
 

+ 16 - 5
MediaBrowser.Dlna/PlayTo/DlnaController.cs

@@ -28,6 +28,7 @@ namespace MediaBrowser.Dlna.PlayTo
         private readonly INetworkManager _networkManager;
         private readonly ILogger _logger;
         private readonly IDlnaManager _dlnaManager;
+        private readonly IUserManager _userManager;
         private bool _playbackStarted = false;
 
         public bool SupportsMediaRemoteControl
@@ -46,7 +47,7 @@ namespace MediaBrowser.Dlna.PlayTo
             }
         }
 
-        public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, INetworkManager networkManager, IDlnaManager dlnaManager)
+        public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, INetworkManager networkManager, IDlnaManager dlnaManager, IUserManager userManager)
         {
             _session = session;
             _itemRepository = itemRepository;
@@ -54,6 +55,7 @@ namespace MediaBrowser.Dlna.PlayTo
             _libraryManager = libraryManager;
             _networkManager = networkManager;
             _dlnaManager = dlnaManager;
+            _userManager = userManager;
             _logger = logger;
         }
 
@@ -194,7 +196,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
         #region SendCommands
 
-        public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
+        public async Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
         {
             _logger.Debug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
 
@@ -227,16 +229,25 @@ namespace MediaBrowser.Dlna.PlayTo
             if (command.PlayCommand == PlayCommand.PlayLast)
             {
                 AddItemsToPlaylist(playlist);
-                return Task.FromResult(true);
             }
             if (command.PlayCommand == PlayCommand.PlayNext)
             {
                 AddItemsToPlaylist(playlist);
-                return Task.FromResult(true);
             }
 
             _logger.Debug("{0} - Playing {1} items", _session.DeviceName, playlist.Count);
-            return PlayItems(playlist);
+
+            if (!string.IsNullOrWhiteSpace(command.ControllingUserId))
+            {
+                var userId = new Guid(command.ControllingUserId);
+
+                var user = _userManager.GetUserById(userId);
+
+                await _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
+                        _session.DeviceName, _session.RemoteEndPoint, user).ConfigureAwait(false);
+            }
+
+            await PlayItems(playlist).ConfigureAwait(false);
         }
 
         public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)

+ 1 - 1
MediaBrowser.Dlna/PlayTo/PlayToManager.cs

@@ -227,7 +227,7 @@ namespace MediaBrowser.Dlna.PlayTo
 
                 if (controller == null)
                 {
-                    sessionInfo.SessionController = controller = new PlayToController(sessionInfo, _sessionManager, _itemRepository, _libraryManager, _logger, _networkManager, _dlnaManager);
+                    sessionInfo.SessionController = controller = new PlayToController(sessionInfo, _sessionManager, _itemRepository, _libraryManager, _logger, _networkManager, _dlnaManager, _userManager);
                 }
 
                 controller.Init(device);

+ 2 - 1
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -494,7 +494,8 @@ namespace MediaBrowser.Model.Dto
         /// </summary>
         /// <value>The part count.</value>
         public int? PartCount { get; set; }
-        public bool? HasAlternateVersions { get; set; }
+        public int? AlternateVersionCount { get; set; }
+        public string PrimaryVersionId { get; set; }
 
         /// <summary>
         /// Determines whether the specified type is type.

+ 6 - 0
MediaBrowser.Model/Session/PlayRequest.cs

@@ -23,6 +23,12 @@ namespace MediaBrowser.Model.Session
         /// </summary>
         /// <value>The play command.</value>
         public PlayCommand PlayCommand { get; set; }
+
+        /// <summary>
+        /// Gets or sets the controlling user identifier.
+        /// </summary>
+        /// <value>The controlling user identifier.</value>
+        public string ControllingUserId { get; set; }
     }
 
     /// <summary>

+ 6 - 0
MediaBrowser.Model/Session/PlaystateCommand.cs

@@ -37,5 +37,11 @@ namespace MediaBrowser.Model.Session
         public PlaystateCommand Command { get; set; }
 
         public long? SeekPositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the controlling user identifier.
+        /// </summary>
+        /// <value>The controlling user identifier.</value>
+        public string ControllingUserId { get; set; }
     }
 }

+ 7 - 8
MediaBrowser.Providers/All/LocalImageProvider.cs

@@ -94,14 +94,13 @@ namespace MediaBrowser.Providers.All
         public List<LocalImageInfo> GetImages(IHasImages item, IEnumerable<string> paths, IDirectoryService directoryService)
         {
             var files = paths.SelectMany(directoryService.GetFiles)
-               .Where(i =>
-               {
-                   var ext = i.Extension;
-
-                   return !string.IsNullOrEmpty(ext) &&
-                       BaseItem.SupportedImageExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
-               })
-               .Cast<FileSystemInfo>()
+                .Where(i =>
+                {
+                    var ext = i.Extension;
+
+                    return !string.IsNullOrEmpty(ext) &&
+                           BaseItem.SupportedImageExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+                })
                .ToList();
 
             var list = new List<LocalImageInfo>();

+ 2 - 1
MediaBrowser.Server.Implementations/Collections/CollectionManager.cs

@@ -111,6 +111,7 @@ namespace MediaBrowser.Server.Implementations.Collections
             }
 
             var list = new List<LinkedChild>();
+            var currentLinkedChildren = collection.GetLinkedChildren().ToList();
 
             foreach (var itemId in ids)
             {
@@ -121,7 +122,7 @@ namespace MediaBrowser.Server.Implementations.Collections
                     throw new ArgumentException("No item exists with the supplied Id");
                 }
 
-                if (collection.LinkedChildren.Any(i => i.ItemId.HasValue && i.ItemId == itemId))
+                if (currentLinkedChildren.Any(i => i.Id == itemId))
                 {
                     throw new ArgumentException("Item already exists in collection");
                 }

+ 6 - 1
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -1082,7 +1082,12 @@ namespace MediaBrowser.Server.Implementations.Dto
                 dto.IsHD = video.IsHD;
 
                 dto.PartCount = video.AdditionalPartIds.Count + 1;
-                dto.HasAlternateVersions = video.HasAlternateVersions;
+                dto.AlternateVersionCount = video.AlternateVersionCount;
+
+                if (video.PrimaryVersionId.HasValue)
+                {
+                    dto.PrimaryVersionId = video.PrimaryVersionId.Value.ToString("N");
+                }
 
                 if (fields.Contains(ItemFields.Chapters))
                 {

+ 1 - 1
MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -410,7 +410,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
 
             if (!string.IsNullOrWhiteSpace(filenamePrefix))
             {
-                if (sortedMovies.All(i => Path.GetFileNameWithoutExtension(i.Path).StartsWith(filenamePrefix, StringComparison.OrdinalIgnoreCase)))
+                if (sortedMovies.Skip(1).All(i => Path.GetFileNameWithoutExtension(i.Path).StartsWith(filenamePrefix + " - ", StringComparison.OrdinalIgnoreCase)))
                 {
                     firstMovie.HasLocalAlternateVersions = true;
 

+ 10 - 0
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -690,6 +690,11 @@ namespace MediaBrowser.Server.Implementations.Session
                 }
             }
 
+            if (session.UserId.HasValue)
+            {
+                command.ControllingUserId = session.UserId.Value.ToString("N");
+            }
+
             return session.SessionController.SendPlayCommand(command, cancellationToken);
         }
 
@@ -723,6 +728,11 @@ namespace MediaBrowser.Server.Implementations.Session
                 throw new ArgumentException(string.Format("Session {0} is unable to seek.", session.Id));
             }
 
+            if (session.UserId.HasValue)
+            {
+                command.ControllingUserId = session.UserId.Value.ToString("N");
+            }
+
             return session.SessionController.SendPlaystateCommand(command, cancellationToken);
         }