diff --git a/MediaBrowser.Api/SessionsService.cs b/MediaBrowser.Api/SessionsService.cs index b8ca70ba5e..df35c93a02 100644 --- a/MediaBrowser.Api/SessionsService.cs +++ b/MediaBrowser.Api/SessionsService.cs @@ -277,7 +277,7 @@ namespace MediaBrowser.Api SeekPositionTicks = request.SeekPositionTicks }; - var task = _sessionManager.SendPlaystateCommand(request.Id, command, CancellationToken.None); + var task = _sessionManager.SendPlaystateCommand(GetSession().Id, request.Id, command, CancellationToken.None); Task.WaitAll(task); } @@ -296,7 +296,7 @@ namespace MediaBrowser.Api ItemType = request.ItemType }; - var task = _sessionManager.SendBrowseCommand(request.Id, command, CancellationToken.None); + var task = _sessionManager.SendBrowseCommand(GetSession().Id, request.Id, command, CancellationToken.None); Task.WaitAll(task); } @@ -307,7 +307,7 @@ namespace MediaBrowser.Api /// The request. public void Post(SendSystemCommand request) { - var task = _sessionManager.SendSystemCommand(request.Id, request.Command, CancellationToken.None); + var task = _sessionManager.SendSystemCommand(GetSession().Id, request.Id, request.Command, CancellationToken.None); Task.WaitAll(task); } @@ -325,7 +325,7 @@ namespace MediaBrowser.Api Text = request.Text }; - var task = _sessionManager.SendMessageCommand(request.Id, command, CancellationToken.None); + var task = _sessionManager.SendMessageCommand(GetSession().Id, request.Id, command, CancellationToken.None); Task.WaitAll(task); } @@ -344,7 +344,7 @@ namespace MediaBrowser.Api StartPositionTicks = request.StartPositionTicks }; - var task = _sessionManager.SendPlayCommand(request.Id, command, CancellationToken.None); + var task = _sessionManager.SendPlayCommand(GetSession().Id, request.Id, command, CancellationToken.None); Task.WaitAll(task); } @@ -367,5 +367,14 @@ namespace MediaBrowser.Api .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .ToList(); } + + private SessionInfo GetSession() + { + var auth = AuthorizationRequestFilterAttribute.GetAuthorization(Request); + + return _sessionManager.Sessions.First(i => string.Equals(i.DeviceId, auth.DeviceId) && + string.Equals(i.Client, auth.Client) && + string.Equals(i.ApplicationVersion, auth.Version)); + } } } \ No newline at end of file diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index b040d3dd81..44c2249896 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -238,6 +238,9 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "HasOfficialRating", Description = "Optional filter by items that have official ratings", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public bool? HasOfficialRating { get; set; } + + [ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool CollapseBoxSetItems { get; set; } } /// @@ -315,6 +318,11 @@ namespace MediaBrowser.Api.UserLibrary items = items.AsEnumerable(); + if (request.CollapseBoxSetItems && AllowBoxSetCollapsing(request)) + { + items = CollapseItemsWithinBoxSets(items, user); + } + items = ApplySortOrder(request, items, user, _libraryManager); // This must be the last filter @@ -1218,6 +1226,41 @@ namespace MediaBrowser.Api.UserLibrary return false; } + private IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, User user) + { + var itemsToCollapse = new List(); + var boxsets = new List(); + + var list = items.ToList(); + + foreach (var item in list.OfType()) + { + var currentBoxSets = item.BoxSetIdList + .Select(i => _libraryManager.GetItemById(i)) + .Where(i => i != null && i.IsVisible(user)) + .ToList(); + + if (currentBoxSets.Count > 0) + { + itemsToCollapse.Add(item); + boxsets.AddRange(currentBoxSets); + } + } + + return list.Except(itemsToCollapse.Cast()).Concat(boxsets).Distinct(); + } + + private bool AllowBoxSetCollapsing(GetItems request) + { + // Only allow when using default sort order + if (!string.IsNullOrEmpty(request.SortBy) && !string.Equals(request.SortBy, "SortName", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + internal static IEnumerable FilterForAdjacency(IEnumerable items, string adjacentToId) { var list = items.ToList(); diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs index fb58e58b7b..ba3b0912b6 100644 --- a/MediaBrowser.Api/VideosService.cs +++ b/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; @@ -22,14 +25,62 @@ 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 + { + [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; } + + /// + /// Gets or sets the id. + /// + /// The id. + [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; } + + /// + /// Gets or sets the id. + /// + /// The id. + [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; } + + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + + [ApiMember(Name = "IsAlternateEncoding", Description = "Filter by versions that are considered alternate encodings of the original.", IsRequired = true, DataType = "bool", ParameterType = "path", Verb = "GET")] + public bool? IsAlternateEncoding { 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; @@ -48,7 +99,7 @@ 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 @@ -58,8 +109,7 @@ namespace MediaBrowser.Api var video = (Video)item; - var items = video.AdditionalPartIds.Select(_libraryManager.GetItemById) - .OrderBy(i => i.SortName) + var items = video.GetAdditionalParts() .Select(i => _dtoService.GetBaseItemDto(i, fields, user, video)) .ToArray(); @@ -71,5 +121,91 @@ namespace MediaBrowser.Api 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 + var fields = Enum.GetNames(typeof(ItemFields)) + .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) + .ToList(); + + var video = (Video)item; + + var items = video.GetAlternateVersions() + .Select(i => _dtoService.GetBaseItemDto(i, fields, user, video)) + .ToArray(); + + var result = new ItemsResult + { + Items = items, + TotalRecordCount = items.Length + }; + + 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(); + 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); + } } } diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs index e147e09056..74ae420950 100644 --- a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs +++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs @@ -14,9 +14,12 @@ namespace MediaBrowser.Controller.Collections public Dictionary ProviderIds { get; set; } + public List ItemIdList { get; set; } + public CollectionCreationOptions() { ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + ItemIdList = new List(); } } } diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index d7bc178ad3..af65bbacaf 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -1,4 +1,5 @@ -using System; +using MediaBrowser.Controller.Entities.Movies; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -11,7 +12,7 @@ namespace MediaBrowser.Controller.Collections /// /// The options. /// Task. - Task CreateCollection(CollectionCreationOptions options); + Task CreateCollection(CollectionCreationOptions options); /// /// Adds to collection. diff --git a/MediaBrowser.Controller/Dlna/DeviceIdentification.cs b/MediaBrowser.Controller/Dlna/DeviceIdentification.cs new file mode 100644 index 0000000000..a3a6155160 --- /dev/null +++ b/MediaBrowser.Controller/Dlna/DeviceIdentification.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Dlna +{ + public class DeviceIdentification + { + /// + /// Gets or sets the name of the friendly. + /// + /// The name of the friendly. + public string FriendlyName { get; set; } + /// + /// Gets or sets the model number. + /// + /// The model number. + public string ModelNumber { get; set; } + /// + /// Gets or sets the serial number. + /// + /// The serial number. + public string SerialNumber { get; set; } + /// + /// Gets or sets the name of the model. + /// + /// The name of the model. + public string ModelName { get; set; } + /// + /// Gets or sets the manufacturer. + /// + /// + /// The manufacturer. + /// + public string Manufacturer { get; set; } + /// + /// Gets or sets the manufacturer URL. + /// + /// The manufacturer URL. + public string ManufacturerUrl { get; set; } + /// + /// Gets or sets the headers. + /// + /// The headers. + public List Headers { get; set; } + + public DeviceIdentification() + { + Headers = new List(); + } + } + + public class HttpHeaderInfo + { + public string Name { get; set; } + public string Value { get; set; } + } +} diff --git a/MediaBrowser.Controller/Dlna/DlnaProfile.cs b/MediaBrowser.Controller/Dlna/DeviceProfile.cs similarity index 64% rename from MediaBrowser.Controller/Dlna/DlnaProfile.cs rename to MediaBrowser.Controller/Dlna/DeviceProfile.cs index 33f95b7944..119cfffd74 100644 --- a/MediaBrowser.Controller/Dlna/DlnaProfile.cs +++ b/MediaBrowser.Controller/Dlna/DeviceProfile.cs @@ -1,7 +1,7 @@  namespace MediaBrowser.Controller.Dlna { - public class DlnaProfile + public class DeviceProfile { /// /// Gets or sets the name. @@ -15,24 +15,6 @@ namespace MediaBrowser.Controller.Dlna /// The type of the client. public string ClientType { get; set; } - /// - /// Gets or sets the name of the friendly. - /// - /// The name of the friendly. - public string FriendlyName { get; set; } - - /// - /// Gets or sets the model number. - /// - /// The model number. - public string ModelNumber { get; set; } - - /// - /// Gets or sets the name of the model. - /// - /// The name of the model. - public string ModelName { get; set; } - /// /// Gets or sets the transcoding profiles. /// @@ -45,7 +27,13 @@ namespace MediaBrowser.Controller.Dlna /// The direct play profiles. public DirectPlayProfile[] DirectPlayProfiles { get; set; } - public DlnaProfile() + /// + /// Gets or sets the identification. + /// + /// The identification. + public DeviceIdentification Identification { get; set; } + + public DeviceProfile() { DirectPlayProfiles = new DirectPlayProfile[] { }; TranscodingProfiles = new TranscodingProfile[] { }; diff --git a/MediaBrowser.Controller/Dlna/DirectPlayProfile.cs b/MediaBrowser.Controller/Dlna/DirectPlayProfile.cs index f1922dd323..8c35b52a81 100644 --- a/MediaBrowser.Controller/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Controller/Dlna/DirectPlayProfile.cs @@ -1,25 +1,97 @@ - +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Xml.Serialization; + namespace MediaBrowser.Controller.Dlna { public class DirectPlayProfile { - public string[] Containers { get; set; } - public string[] AudioCodecs { get; set; } - public string[] VideoCodecs { get; set; } + public string Container { get; set; } + public string AudioCodec { get; set; } + public string VideoCodec { get; set; } + + [IgnoreDataMember] + [XmlIgnore] + public string[] Containers + { + get + { + return (Container ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + set + { + Container = value == null ? null : string.Join(",", value); + } + } + + [IgnoreDataMember] + [XmlIgnore] + public string[] AudioCodecs + { + get + { + return (AudioCodec ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + set + { + AudioCodec = value == null ? null : string.Join(",", value); + } + } + + [IgnoreDataMember] + [XmlIgnore] + public string[] VideoCodecs + { + get + { + return (VideoCodec ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + set + { + VideoCodec = value == null ? null : string.Join(",", value); + } + } + public string MimeType { get; set; } public DlnaProfileType Type { get; set; } + public List Conditions { get; set; } + public DirectPlayProfile() { - Containers = new string[] { }; - AudioCodecs = new string[] { }; - VideoCodecs = new string[] { }; + Conditions = new List(); } } + public class ProfileCondition + { + public ProfileConditionType Condition { get; set; } + public ProfileConditionValue Value { get; set; } + } + public enum DlnaProfileType { Audio = 0, Video = 1 } + + public enum ProfileConditionType + { + Equals = 0, + NotEquals = 1, + LessThanEqual = 2, + GreaterThanEqual = 3 + } + + public enum ProfileConditionValue + { + AudioChannels, + AudioBitrate, + Filesize, + VideoWidth, + VideoHeight, + VideoBitrate, + VideoFramerate + } } diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs index 017dbc8746..6de17e5511 100644 --- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs +++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs @@ -8,21 +8,19 @@ namespace MediaBrowser.Controller.Dlna /// Gets the dlna profiles. /// /// IEnumerable{DlnaProfile}. - IEnumerable GetProfiles(); + IEnumerable GetProfiles(); /// /// Gets the default profile. /// /// DlnaProfile. - DlnaProfile GetDefaultProfile(); + DeviceProfile GetDefaultProfile(); /// /// Gets the profile. /// - /// Name of the friendly. - /// Name of the model. - /// The model number. - /// DlnaProfile. - DlnaProfile GetProfile(string friendlyName, string modelName, string modelNumber); + /// The device information. + /// DeviceProfile. + DeviceProfile GetProfile(DeviceIdentification deviceInfo); } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index e0c792307e..be64d20c33 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -954,6 +954,83 @@ namespace MediaBrowser.Controller.Entities return (DateTime.UtcNow - DateCreated).TotalDays < ConfigurationManager.Configuration.RecentItemDays; } + /// + /// Gets the linked child. + /// + /// The info. + /// BaseItem. + 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; + } + /// /// Adds a person to the item /// diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index ee371680ef..45daaba0b2 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -354,18 +354,43 @@ 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; + } + } } } @@ -898,83 +923,6 @@ namespace MediaBrowser.Controller.Entities .Where(i => i != null); } - /// - /// Gets the linked child. - /// - /// The info. - /// BaseItem. - 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 RefreshedOwnedItems(MetadataRefreshOptions options, List fileSystemChildren, CancellationToken cancellationToken) { var changesFound = false; diff --git a/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs b/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs new file mode 100644 index 0000000000..0fd463155f --- /dev/null +++ b/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Entities +{ + /// + /// Marker interface to denote a class that supports being hidden underneath it's boxset. + /// Just about anything can be placed into a boxset, + /// but movies should also only appear underneath and not outside separately (subject to configuration). + /// + public interface ISupportsBoxSetGrouping + { + /// + /// Gets or sets the box set identifier list. + /// + /// The box set identifier list. + List BoxSetIdList { get; set; } + } +} diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 9858dd5a99..f53b676105 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -1,11 +1,11 @@ -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; @@ -14,7 +14,7 @@ namespace MediaBrowser.Controller.Entities.Movies /// /// Class Movie /// - public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasKeywords, IHasTrailers, IHasThemeMedia, IHasTaglines, IHasPreferredMetadataLanguage, IHasAwards, IHasMetascore, IHasLookupInfo + public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasBudget, IHasKeywords, IHasTrailers, IHasThemeMedia, IHasTaglines, IHasPreferredMetadataLanguage, IHasAwards, IHasMetascore, IHasLookupInfo, ISupportsBoxSetGrouping { public List SpecialFeatureIds { get; set; } @@ -23,6 +23,12 @@ namespace MediaBrowser.Controller.Entities.Movies public List ThemeSongIds { get; set; } public List ThemeVideoIds { get; set; } + /// + /// This is just a cache to enable quick access by Id + /// + [IgnoreDataMember] + public List BoxSetIdList { get; set; } + /// /// Gets or sets the preferred metadata country code. /// @@ -39,6 +45,7 @@ namespace MediaBrowser.Controller.Entities.Movies LocalTrailerIds = new List(); ThemeSongIds = new List(); ThemeVideoIds = new List(); + BoxSetIdList = new List(); Taglines = new List(); Keywords = new List(); } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 10034d7e5f..18db21f382 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/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; @@ -19,15 +20,64 @@ namespace MediaBrowser.Controller.Entities public class Video : BaseItem, IHasMediaStreams, IHasAspectRatio, IHasTags, ISupportsPlaceHolders { public bool IsMultiPart { get; set; } + public bool HasLocalAlternateVersions { get; set; } + public Guid? PrimaryVersionId { get; set; } public List AdditionalPartIds { get; set; } + public List LocalAlternateVersionIds { get; set; } public Video() { PlayableStreamFileNames = new List(); AdditionalPartIds = new List(); + LocalAlternateVersionIds = new List(); Tags = new List(); SubtitleFiles = new List(); + LinkedAlternateVersions = new List(); + } + + [IgnoreDataMember] + public int AlternateVersionCount + { + get + { + return LinkedAlternateVersions.Count + LocalAlternateVersionIds.Count; + } + } + + public List LinkedAlternateVersions { get; set; } + + /// + /// Gets the linked children. + /// + /// IEnumerable{BaseItem}. + public IEnumerable GetAlternateVersions() + { + var filesWithinSameDirectory = LocalAlternateVersionIds + .Select(i => LibraryManager.GetItemById(i)) + .Where(i => i != null) + .OfType