diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index b040d3dd81..ec00be9882 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) + { + 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.Controller/Dlna/DlnaProfile.cs b/MediaBrowser.Controller/Dlna/DeviceProfile.cs similarity index 96% rename from MediaBrowser.Controller/Dlna/DlnaProfile.cs rename to MediaBrowser.Controller/Dlna/DeviceProfile.cs index 33f95b7944..3fecf957b7 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. @@ -45,7 +45,7 @@ namespace MediaBrowser.Controller.Dlna /// The direct play profiles. public DirectPlayProfile[] DirectPlayProfiles { get; set; } - public DlnaProfile() + 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..04f6588055 100644 --- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs +++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs @@ -8,13 +8,13 @@ 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. @@ -23,6 +23,6 @@ namespace MediaBrowser.Controller.Dlna /// Name of the model. /// The model number. /// DlnaProfile. - DlnaProfile GetProfile(string friendlyName, string modelName, string modelNumber); + DeviceProfile GetProfile(string friendlyName, string modelName, string modelNumber); } } 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/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 21a501b08e..7e5e6d9b0c 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -75,7 +75,7 @@ - + @@ -114,6 +114,7 @@ + diff --git a/MediaBrowser.Dlna/DlnaManager.cs b/MediaBrowser.Dlna/DlnaManager.cs index 1c9cba2bea..91d205b476 100644 --- a/MediaBrowser.Dlna/DlnaManager.cs +++ b/MediaBrowser.Dlna/DlnaManager.cs @@ -1,4 +1,7 @@ -using MediaBrowser.Controller.Dlna; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Model.Serialization; using System.Collections.Generic; using System.Text.RegularExpressions; @@ -6,11 +9,23 @@ namespace MediaBrowser.Dlna { public class DlnaManager : IDlnaManager { - public IEnumerable GetProfiles() + private IApplicationPaths _appPaths; + private readonly IXmlSerializer _xmlSerializer; + private readonly IFileSystem _fileSystem; + + public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem) + { + _xmlSerializer = xmlSerializer; + _fileSystem = fileSystem; + + //GetProfiles(); + } + + public IEnumerable GetProfiles() { - var list = new List(); + var list = new List(); - list.Add(new DlnaProfile + list.Add(new DeviceProfile { Name = "Samsung TV (B Series)", ClientType = "DLNA", @@ -59,7 +74,7 @@ namespace MediaBrowser.Dlna } }); - list.Add(new DlnaProfile + list.Add(new DeviceProfile { Name = "Samsung TV (E/F-series)", ClientType = "DLNA", @@ -107,7 +122,7 @@ namespace MediaBrowser.Dlna } }); - list.Add(new DlnaProfile + list.Add(new DeviceProfile { Name = "Samsung TV (C/D-series)", ClientType = "DLNA", @@ -154,7 +169,7 @@ namespace MediaBrowser.Dlna } }); - list.Add(new DlnaProfile + list.Add(new DeviceProfile { Name = "Xbox 360", ClientType = "DLNA", @@ -189,7 +204,7 @@ namespace MediaBrowser.Dlna } }); - list.Add(new DlnaProfile + list.Add(new DeviceProfile { Name = "Xbox One", ModelName = "Xbox One", @@ -225,7 +240,7 @@ namespace MediaBrowser.Dlna } }); - list.Add(new DlnaProfile + list.Add(new DeviceProfile { Name = "Sony Bravia (2012)", ClientType = "DLNA", @@ -262,7 +277,7 @@ namespace MediaBrowser.Dlna }); //WDTV does not need any transcoding of the formats we support statically - list.Add(new DlnaProfile + list.Add(new DeviceProfile { Name = "WDTV Live", ClientType = "DLNA", @@ -284,7 +299,7 @@ namespace MediaBrowser.Dlna } }); - list.Add(new DlnaProfile + list.Add(new DeviceProfile { //Linksys DMA2100us does not need any transcoding of the formats we support statically Name = "Linksys DMA2100", @@ -307,12 +322,17 @@ namespace MediaBrowser.Dlna } }); + foreach (var item in list) + { + //_xmlSerializer.SerializeToFile(item, "d:\\" + _fileSystem.GetValidFilename(item.Name)); + } + return list; } - public DlnaProfile GetDefaultProfile() + public DeviceProfile GetDefaultProfile() { - return new DlnaProfile + return new DeviceProfile { TranscodingProfiles = new[] { @@ -345,7 +365,7 @@ namespace MediaBrowser.Dlna }; } - public DlnaProfile GetProfile(string friendlyName, string modelName, string modelNumber) + public DeviceProfile GetProfile(string friendlyName, string modelName, string modelNumber) { foreach (var profile in GetProfiles()) { diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs b/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs index cfb2c7d1ce..4f776807e2 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs +++ b/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Dlna.PlayTo public long StartPositionTicks { get; set; } - public static PlaylistItem Create(BaseItem item, DlnaProfile profile) + public static PlaylistItem Create(BaseItem item, DeviceProfile profile) { var playlistItem = new PlaylistItem { @@ -92,7 +92,7 @@ namespace MediaBrowser.Dlna.PlayTo return true; } - private static bool IsSupported(DlnaProfile profile, TranscodingProfile transcodingProfile, string path) + private static bool IsSupported(DeviceProfile profile, TranscodingProfile transcodingProfile, string path) { // Placeholder for future conditions return true; diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs index 9a196cc47a..8e70c1d3dc 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs @@ -126,6 +126,18 @@ namespace MediaBrowser.Server.Implementations.Collections ItemType = item.GetType().Name, Type = LinkedChildType.Manual }); + + var supportsGrouping = item as ISupportsBoxSetGrouping; + + if (supportsGrouping != null) + { + var boxsetIdList = supportsGrouping.BoxSetIdList.ToList(); + if (!boxsetIdList.Contains(collectionId)) + { + boxsetIdList.Add(collectionId); + } + supportsGrouping.BoxSetIdList = boxsetIdList; + } } collection.LinkedChildren.AddRange(list); @@ -156,6 +168,16 @@ namespace MediaBrowser.Server.Implementations.Collections } list.Add(child); + + var childItem = _libraryManager.GetItemById(itemId); + var supportsGrouping = childItem as ISupportsBoxSetGrouping; + + if (supportsGrouping != null) + { + var boxsetIdList = supportsGrouping.BoxSetIdList.ToList(); + boxsetIdList.Remove(collectionId); + supportsGrouping.BoxSetIdList = boxsetIdList; + } } var shortcutFiles = Directory diff --git a/MediaBrowser.Server.Implementations/Library/Validators/BoxSetPostScanTask.cs b/MediaBrowser.Server.Implementations/Library/Validators/BoxSetPostScanTask.cs new file mode 100644 index 0000000000..f02c907c66 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Library/Validators/BoxSetPostScanTask.cs @@ -0,0 +1,50 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Library.Validators +{ + public class BoxSetPostScanTask : ILibraryPostScanTask + { + private readonly ILibraryManager _libraryManager; + + public BoxSetPostScanTask(ILibraryManager libraryManager) + { + _libraryManager = libraryManager; + } + + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + var items = _libraryManager.RootFolder.RecursiveChildren.ToList(); + + var boxsets = items.OfType().ToList(); + + var numComplete = 0; + + foreach (var boxset in boxsets) + { + foreach (var child in boxset.GetLinkedChildren().OfType()) + { + var boxsetIdList = child.BoxSetIdList.ToList(); + if (!boxsetIdList.Contains(boxset.Id)) + { + boxsetIdList.Add(boxset.Id); + } + child.BoxSetIdList = boxsetIdList; + } + + numComplete++; + double percent = numComplete; + percent /= boxsets.Count; + progress.Report(percent * 100); + } + + progress.Report(100); + return Task.FromResult(true); + } + } +} diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index c44b608458..8715651331 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -165,6 +165,7 @@ + diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 32932fe0ba..f55f18cc37 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -492,7 +492,7 @@ namespace MediaBrowser.ServerApplication var appThemeManager = new AppThemeManager(ApplicationPaths, FileSystemManager, JsonSerializer, Logger); RegisterSingleInstance(appThemeManager); - var dlnaManager = new DlnaManager(); + var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager); RegisterSingleInstance(dlnaManager); var collectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor);