diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index 475183dea9..b4d2e2f0f2 100644 --- a/MediaBrowser.Api/PlaylistService.cs +++ b/MediaBrowser.Api/PlaylistService.cs @@ -1,9 +1,12 @@ using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Playlists; using MediaBrowser.Model.Querying; using ServiceStack; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -18,6 +21,9 @@ namespace MediaBrowser.Api [ApiMember(Name = "Ids", Description = "Item Ids to add to the playlist", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] public string Ids { get; set; } + + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string UserId { get; set; } } [Route("/Playlists/{Id}/Items", "POST", Summary = "Adds items to a playlist")] @@ -37,16 +43,55 @@ namespace MediaBrowser.Api public string Id { get; set; } } + [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")] + public class GetPlaylistItems : IReturn>, IHasItemFields + { + [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Id { get; set; } + + /// + /// Gets or sets the user id. + /// + /// The user id. + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid? UserId { get; set; } + + /// + /// Skips over a given number of items within the results. Use for paging. + /// + /// The start index. + [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? StartIndex { get; set; } + + /// + /// The maximum number of items to return + /// + /// The limit. + [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? Limit { get; set; } + + /// + /// Fields to return within the items, in addition to basic information + /// + /// The fields. + [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Fields { get; set; } + } + [Authenticated] public class PlaylistService : BaseApiService { private readonly IPlaylistManager _playlistManager; private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; - public PlaylistService(IDtoService dtoService, IPlaylistManager playlistManager) + public PlaylistService(IDtoService dtoService, IPlaylistManager playlistManager, IUserManager userManager, ILibraryManager libraryManager) { _dtoService = dtoService; _playlistManager = playlistManager; + _userManager = userManager; + _libraryManager = libraryManager; } public object Post(CreatePlaylist request) @@ -54,7 +99,8 @@ namespace MediaBrowser.Api var task = _playlistManager.CreatePlaylist(new PlaylistCreationOptions { Name = request.Name, - ItemIdList = (request.Ids ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList() + ItemIdList = (request.Ids ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(), + UserId = request.UserId }); var item = task.Result; @@ -80,5 +126,36 @@ namespace MediaBrowser.Api //Task.WaitAll(task); } + + public object Get(GetPlaylistItems request) + { + var playlist = (Playlist)_libraryManager.GetItemById(request.Id); + var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null; + var items = playlist.GetManageableItems().ToArray(); + + var count = items.Length; + + if (request.StartIndex.HasValue) + { + items = items.Skip(request.StartIndex.Value).ToArray(); + } + + if (request.Limit.HasValue) + { + items = items.Take(request.Limit.Value).ToArray(); + } + + var dtos = items + .Select(i => _dtoService.GetBaseItemDto(i, request.GetItemFields().ToList(), user)) + .ToArray(); + + var result = new ItemsResult + { + Items = dtos, + TotalRecordCount = count + }; + + return ToOptimizedResult(result); + } } } diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index 7cd518a187..f236100147 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using ServiceStack; using System; +using System.Collections.Generic; +using System.Linq; namespace MediaBrowser.Api.UserLibrary { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 2bdbab0848..36e65f5f53 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1006,6 +1006,18 @@ namespace MediaBrowser.Controller.Entities private BaseItem FindLinkedChild(LinkedChild info) { + if (!string.IsNullOrWhiteSpace(info.ItemName)) + { + if (string.Equals(info.ItemType, "musicgenre", StringComparison.OrdinalIgnoreCase)) + { + return LibraryManager.GetMusicGenre(info.ItemName); + } + if (string.Equals(info.ItemType, "musicartist", StringComparison.OrdinalIgnoreCase)) + { + return LibraryManager.GetArtist(info.ItemName); + } + } + if (!string.IsNullOrEmpty(info.Path)) { var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path); @@ -1028,7 +1040,17 @@ namespace MediaBrowser.Controller.Entities { if (info.ItemYear.HasValue) { - return info.ItemYear.Value == (i.ProductionYear ?? -1); + if (info.ItemYear.Value != (i.ProductionYear ?? -1)) + { + return false; + } + } + if (info.ItemIndexNumber.HasValue) + { + if (info.ItemIndexNumber.Value != (i.IndexNumber ?? -1)) + { + return false; + } } return true; } diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs index fa2b49a60b..b30bd81b96 100644 --- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -7,11 +7,6 @@ namespace MediaBrowser.Controller.Entities /// public abstract class BasePluginFolder : Folder, ICollectionFolder, IByReferenceItem { - protected BasePluginFolder() - { - DisplayMediaType = "CollectionFolder"; - } - public virtual string CollectionType { get { return null; } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 12afe26b63..2013b926cc 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -38,6 +38,12 @@ namespace MediaBrowser.Controller.Entities Tags = new List(); } + [IgnoreDataMember] + public virtual bool IsPreSorted + { + get { return false; } + } + /// /// Gets a value indicating whether this instance is folder. /// @@ -855,7 +861,7 @@ namespace MediaBrowser.Controller.Entities /// if set to true [include linked children]. /// IEnumerable{BaseItem}. /// - public IEnumerable GetRecursiveChildren(User user, bool includeLinkedChildren = true) + public virtual IEnumerable GetRecursiveChildren(User user, bool includeLinkedChildren = true) { if (user == null) { diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index 1ae04e40f9..3fc0ab716e 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -12,12 +12,25 @@ namespace MediaBrowser.Controller.Entities public string ItemName { get; set; } public string ItemType { get; set; } public int? ItemYear { get; set; } + public int? ItemIndexNumber { get; set; } /// /// Serves as a cache /// [IgnoreDataMember] public Guid? ItemId { get; set; } + + public static LinkedChild Create(BaseItem item) + { + return new LinkedChild + { + ItemName = item.Name, + ItemYear = item.ProductionYear, + ItemType = item.GetType().Name, + Type = LinkedChildType.Manual, + ItemIndexNumber = item.IndexNumber + }; + } } public enum LinkedChildType diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 0d2be9f740..5e6bd97079 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Progress; +using System.Runtime.Serialization; +using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; @@ -58,6 +59,15 @@ namespace MediaBrowser.Controller.Entities.Movies return config.BlockUnratedItems.Contains(UnratedItem.Movie); } + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + public override IEnumerable GetChildren(User user, bool includeLinkedChildren) { var children = base.GetChildren(user, includeLinkedChildren); diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index cf39cda899..3977d869c2 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -29,6 +29,15 @@ namespace MediaBrowser.Controller.Entities.TV } } + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + /// /// We want to group into our Series /// diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 9c2ed27bb8..27ca8b18da 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -39,6 +39,15 @@ namespace MediaBrowser.Controller.Entities.TV DisplaySpecialsWithSeasons = true; } + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + public bool DisplaySpecialsWithSeasons { get; set; } public List LocalTrailerIds { get; set; } diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index f8ca56fa88..34ca85d1d7 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -1,7 +1,6 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.Entities; -using MoreLinq; using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index e20387eba3..5ea535f4d5 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -1,20 +1,87 @@ using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; namespace MediaBrowser.Controller.Playlists { public class Playlist : Folder { - public List ItemIds { get; set; } - - public Playlist() - { - ItemIds = new List(); - } + public string OwnerUserId { get; set; } public override IEnumerable GetChildren(User user, bool includeLinkedChildren) { - return base.GetChildren(user, includeLinkedChildren); + return GetPlayableItems(user); + } + + public override IEnumerable GetRecursiveChildren(User user, bool includeLinkedChildren = true) + { + return GetPlayableItems(user); + } + + public IEnumerable GetManageableItems() + { + return GetLinkedChildren(); + } + + private IEnumerable GetPlayableItems(User user) + { + return GetPlaylistItems(MediaType, base.GetChildren(user, true), user); + } + + public static IEnumerable GetPlaylistItems(string playlistMediaType, IEnumerable inputItems, User user) + { + return inputItems.SelectMany(i => + { + var folder = i as Folder; + + if (folder != null) + { + var items = folder.GetRecursiveChildren(user, true) + .Where(m => !m.IsFolder && string.Equals(m.MediaType, playlistMediaType, StringComparison.OrdinalIgnoreCase)); + + if (!folder.IsPreSorted) + { + items = LibraryManager.Sort(items, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending); + } + + return items; + } + + return new[] { i }; + }); + } + + [IgnoreDataMember] + public override bool IsPreSorted + { + get + { + return true; + } + } + + public string PlaylistMediaType { get; set; } + + public override string MediaType + { + get + { + return PlaylistMediaType; + } + } + + public void SetMediaType(string value) + { + PlaylistMediaType = value; + } + + public override bool IsVisible(User user) + { + return base.IsVisible(user) && string.Equals(user.Id.ToString("N"), OwnerUserId); } } } diff --git a/MediaBrowser.Controller/Playlists/PlaylistCreationOptions.cs b/MediaBrowser.Controller/Playlists/PlaylistCreationOptions.cs index a62cbe12e5..1766ba75ca 100644 --- a/MediaBrowser.Controller/Playlists/PlaylistCreationOptions.cs +++ b/MediaBrowser.Controller/Playlists/PlaylistCreationOptions.cs @@ -8,6 +8,10 @@ namespace MediaBrowser.Controller.Playlists public List ItemIdList { get; set; } + public string MediaType { get; set; } + + public string UserId { get; set; } + public PlaylistCreationOptions() { ItemIdList = new List(); diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs index 3cb90d360e..7e6895ec5f 100644 --- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs +++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs @@ -1283,6 +1283,78 @@ namespace MediaBrowser.Controller.Providers return new[] { personInfo }; } + protected LinkedChild GetLinkedChild(XmlReader reader) + { + reader.MoveToContent(); + + var linkedItem = new LinkedChild + { + Type = LinkedChildType.Manual + }; + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Name": + { + linkedItem.ItemName = reader.ReadElementContentAsString(); + break; + } + + case "Type": + { + linkedItem.ItemType = reader.ReadElementContentAsString(); + break; + } + + case "Year": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval)) + { + linkedItem.ItemYear = rval; + } + } + + break; + } + + case "IndexNumber": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval)) + { + linkedItem.ItemIndexNumber = rval; + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + + return string.IsNullOrWhiteSpace(linkedItem.ItemName) || string.IsNullOrWhiteSpace(linkedItem.ItemType) ? null : linkedItem; + } + + /// /// Used to split names of comma or pipe delimeted genres and people /// diff --git a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs index aced2009d1..4eb6baeed2 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs +++ b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs @@ -462,7 +462,7 @@ namespace MediaBrowser.Dlna.ContentDirectory items = FilterUnsupportedContent(items); - if (folder is Series || folder is Season || folder is BoxSet) + if (folder.IsPreSorted) { return items; } diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 2cc7e989b4..b103d9f5a0 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -83,6 +83,7 @@ + diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs index 51a4684d70..85a72cf281 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs @@ -71,59 +71,5 @@ namespace MediaBrowser.LocalMetadata.Parsers item.LinkedChildren = list; } - - private LinkedChild GetLinkedChild(XmlReader reader) - { - reader.MoveToContent(); - - var linkedItem = new LinkedChild - { - Type = LinkedChildType.Manual - }; - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Name": - { - linkedItem.ItemName = reader.ReadElementContentAsString(); - break; - } - - case "Type": - { - linkedItem.ItemType = reader.ReadElementContentAsString(); - break; - } - - case "Year": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - int rval; - - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) - { - linkedItem.ItemYear = rval; - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - } - - return string.IsNullOrWhiteSpace(linkedItem.ItemName) || string.IsNullOrWhiteSpace(linkedItem.ItemType) ? null : linkedItem; - } } } diff --git a/MediaBrowser.LocalMetadata/Savers/FolderXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/FolderXmlSaver.cs index 6dd65b69c5..c38a33c406 100644 --- a/MediaBrowser.LocalMetadata/Savers/FolderXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/FolderXmlSaver.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; namespace MediaBrowser.LocalMetadata.Savers { @@ -37,7 +38,8 @@ namespace MediaBrowser.LocalMetadata.Savers { if (!(item is Series) && !(item is BoxSet) && !(item is MusicArtist) && !(item is MusicAlbum) && !(item is Season) && - !(item is GameSystem)) + !(item is GameSystem) && + !(item is Playlist)) { return updateType >= ItemUpdateType.MetadataDownload; } diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs new file mode 100644 index 0000000000..cdb3a2500f --- /dev/null +++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs @@ -0,0 +1,68 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; + +namespace MediaBrowser.LocalMetadata.Savers +{ + public class PlaylistXmlSaver : IMetadataFileSaver + { + public string Name + { + get + { + return "Media Browser Xml"; + } + } + + /// + /// Determines whether [is enabled for] [the specified item]. + /// + /// The item. + /// Type of the update. + /// true if [is enabled for] [the specified item]; otherwise, false. + public bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType) + { + if (!item.SupportsLocalMetadata) + { + return false; + } + + return item is BoxSet && updateType >= ItemUpdateType.MetadataDownload; + } + + /// + /// Saves the specified item. + /// + /// The item. + /// The cancellation token. + /// Task. + public void Save(IHasMetadata item, CancellationToken cancellationToken) + { + var builder = new StringBuilder(); + + builder.Append(""); + + XmlSaverHelpers.AddCommonNodes((BoxSet)item, builder); + + builder.Append(""); + + var xmlFilePath = GetSavePath(item); + + XmlSaverHelpers.Save(builder, xmlFilePath, new List { }); + } + + /// + /// Gets the save path. + /// + /// The item. + /// System.String. + public string GetSavePath(IHasMetadata item) + { + return Path.Combine(item.Path, "playlist.xml"); + } + } +} diff --git a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs index 1a2c341da1..491592989c 100644 --- a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs +++ b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Entities; namespace MediaBrowser.LocalMetadata.Savers @@ -109,7 +110,8 @@ namespace MediaBrowser.LocalMetadata.Savers "VoteCount", "Website", "Zap2ItId", - "CollectionItems" + "CollectionItems", + "PlaylistItems" }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); @@ -631,10 +633,16 @@ namespace MediaBrowser.LocalMetadata.Savers builder.Append(""); } - var folder = item as BoxSet; - if (folder != null) + var boxset = item as BoxSet; + if (boxset != null) { - AddCollectionItems(folder, builder); + AddLinkedChildren(boxset, builder, "CollectionItems", "CollectionItem"); + } + + var playlist = item as Playlist; + if (playlist != null) + { + AddLinkedChildren(playlist, builder, "PlaylistItems", "PlaylistItem"); } } @@ -693,7 +701,7 @@ namespace MediaBrowser.LocalMetadata.Savers } } - public static void AddCollectionItems(Folder item, StringBuilder builder) + public static void AddLinkedChildren(Folder item, StringBuilder builder, string pluralNodeName, string singularNodeName) { var items = item.LinkedChildren .Where(i => i.Type == LinkedChildType.Manual && !string.IsNullOrWhiteSpace(i.ItemName)) @@ -704,10 +712,10 @@ namespace MediaBrowser.LocalMetadata.Savers return; } - builder.Append(""); + builder.Append("<" + pluralNodeName + ">"); foreach (var link in items) { - builder.Append(""); + builder.Append("<" + singularNodeName + ">"); builder.Append("" + SecurityElement.Escape(link.ItemName) + ""); builder.Append("" + SecurityElement.Escape(link.ItemType) + ""); @@ -717,9 +725,14 @@ namespace MediaBrowser.LocalMetadata.Savers builder.Append("" + SecurityElement.Escape(link.ItemYear.Value.ToString(UsCulture)) + ""); } - builder.Append(""); + if (link.ItemIndexNumber.HasValue) + { + builder.Append("" + SecurityElement.Escape(link.ItemIndexNumber.Value.ToString(UsCulture)) + ""); + } + + builder.Append(""); } - builder.Append(""); + builder.Append(""); } } } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index d138ddf9bc..9581b57407 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -594,7 +594,19 @@ namespace MediaBrowser.Model.Dto /// /// The parent thumb image tag. public string ParentThumbImageTag { get; set; } - + + /// + /// Gets or sets the parent primary image item identifier. + /// + /// The parent primary image item identifier. + public string ParentPrimaryImageItemId { get; set; } + + /// + /// Gets or sets the parent primary image tag. + /// + /// The parent primary image tag. + public string ParentPrimaryImageTag { get; set; } + /// /// Gets or sets the chapters. /// diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs index af1bd94271..567092caec 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs @@ -239,7 +239,7 @@ namespace MediaBrowser.Server.Implementations.Channels throw new ApplicationException("Unexpected response type encountered: " + response.ContentType); } - File.Move(response.TempFilePath, destination); + File.Copy(response.TempFilePath, destination, true); await RefreshMediaSourceItem(destination, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs index 6a87453ab6..fe4b16645c 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs @@ -162,13 +162,7 @@ namespace MediaBrowser.Server.Implementations.Collections throw new ArgumentException("Item already exists in collection"); } - list.Add(new LinkedChild - { - ItemName = item.Name, - ItemYear = item.ProductionYear, - ItemType = item.GetType().Name, - Type = LinkedChildType.Manual - }); + list.Add(LinkedChild.Create(item)); var supportsGrouping = item as ISupportsBoxSetGrouping; diff --git a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs index aaa02c7209..b02c52874c 100644 --- a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs +++ b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs @@ -8,6 +8,7 @@ namespace MediaBrowser.Server.Implementations.Collections public ManualCollectionsFolder() { Name = "Collections"; + DisplayMediaType = "CollectionFolder"; } public override bool IsVisible(User user) diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index afcfde556b..e3a386841b 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Sync; using MediaBrowser.Model.Drawing; @@ -179,6 +180,11 @@ namespace MediaBrowser.Server.Implementations.Dto } } + if (item is Playlist) + { + AttachLinkedChildImages(dto, (Folder)item, user); + } + return dto; } @@ -819,7 +825,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.DisplayOrder = hasDisplayOrder.DisplayOrder; } - var collectionFolder = item as CollectionFolder; + var collectionFolder = item as ICollectionFolder; if (collectionFolder != null) { dto.CollectionType = collectionFolder.CollectionType; @@ -1211,6 +1217,45 @@ namespace MediaBrowser.Server.Implementations.Dto } } + private void AttachLinkedChildImages(BaseItemDto dto, Folder folder, User user) + { + List linkedChildren = null; + + if (dto.BackdropImageTags.Count == 0) + { + if (linkedChildren == null) + { + linkedChildren = user == null + ? folder.GetRecursiveChildren().ToList() + : folder.GetRecursiveChildren(user, true).ToList(); + } + var parentWithBackdrop = linkedChildren.FirstOrDefault(i => i.GetImages(ImageType.Backdrop).Any()); + + if (parentWithBackdrop != null) + { + dto.ParentBackdropItemId = GetDtoId(parentWithBackdrop); + dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop); + } + } + + if (!dto.ImageTags.ContainsKey(ImageType.Primary)) + { + if (linkedChildren == null) + { + linkedChildren = user == null + ? folder.GetRecursiveChildren().ToList() + : folder.GetRecursiveChildren(user, true).ToList(); + } + var parentWithImage = linkedChildren.FirstOrDefault(i => i.GetImages(ImageType.Primary).Any()); + + if (parentWithImage != null) + { + dto.ParentPrimaryImageItemId = GetDtoId(parentWithImage); + dto.ParentPrimaryImageTag = GetImageCacheTag(parentWithImage, ImageType.Primary); + } + } + } + private string GetMappedPath(IHasMetadata item) { var path = item.Path; diff --git a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs index 4e182ea88f..1c4ccb1414 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -75,7 +75,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization public bool IsEnabled { - get { return !GetTvOptions().IsEnabled; } + get { return GetTvOptions().IsEnabled; } } } } diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/PlaylistResolver.cs new file mode 100644 index 0000000000..7eff53ce1f --- /dev/null +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -0,0 +1,38 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using System; +using System.IO; + +namespace MediaBrowser.Server.Implementations.Library.Resolvers +{ + public class PlaylistResolver : FolderResolver + { + /// + /// Resolves the specified args. + /// + /// The args. + /// BoxSet. + protected override Playlist Resolve(ItemResolveArgs args) + { + // It's a boxset if all of the following conditions are met: + // Is a Directory + // Contains [playlist] in the path + if (args.IsDirectory) + { + var filename = Path.GetFileName(args.Path); + + if (string.IsNullOrEmpty(filename)) + { + return null; + } + + if (filename.IndexOf("[playlist]", StringComparison.OrdinalIgnoreCase) != -1) + { + return new Playlist { Path = args.Path }; + } + } + + return null; + } + } +} diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json index 3a5b91abd9..3c1748ef1b 100644 --- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -323,6 +323,13 @@ "HeaderSelectPlayer": "Select Player:", "ButtonSelect": "Select", "ButtonNew": "New", - "MessageInternetExplorerWebm": "For best results with Internet Explorer please install the WebM plugin for IE.", - "HeaderVideoError": "Video Error" + "MessageInternetExplorerWebm": "For best results with Internet Explorer please install the WebM plugin for IE.", + "HeaderVideoError": "Video Error", + "ButtonAddToPlaylist": "Add to playlist", + "HeaderAddToPlaylist": "Add to Playlist", + "LabelName": "Name:", + "ButtonSubmit": "Submit", + "LabelSelectPlaylist": "Playlist:", + "OptionNewPlaylist": "New playlist...", + "MessageAddedToPlaylistSuccess": "Ok" } diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json index caf8860fc5..4716209481 100644 --- a/MediaBrowser.Server.Implementations/Localization/Server/server.json +++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json @@ -808,6 +808,8 @@ "TabNextUp": "Next Up", "MessageNoMovieSuggestionsAvailable": "No movie suggestions are currently available. Start watching and rating your movies, and then come back to view your recommendations.", "MessageNoCollectionsAvailable": "Collections allow you to enjoy personalized groupings of Movies, Series, Albums, Books and Games. Click the New button to start creating Collections.", + "MessageNoPlaylistsAvailable": "Playlists allow you to create lists of content to play consecutively at a time. To add items to playlists, right click or tap and hold, then select Add to Playlist.", + "MessageNoPlaylistItemsAvailable": "This playlist is currently empty.", "HeaderWelcomeToMediaBrowserWebClient": "Welcome to the Media Browser Web Client", "ButtonDismiss": "Dismiss", "MessageLearnHowToCustomize": "Learn how to customize this page to your own personal tastes. Click your user icon in the top right corner of the screen to view and update your preferences.", @@ -915,5 +917,7 @@ "OptionProtocolHls": "Http Live Streaming", "LabelContext": "Context:", "OptionContextStreaming": "Streaming", - "OptionContextStatic": "Sync" + "OptionContextStatic": "Sync", + "ButtonAddToPlaylist": "Add to playlist", + "TabPlaylists": "Playlists" } diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 8bfbc08550..c60835ee6a 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -166,6 +166,7 @@ + diff --git a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs index 3b46191b1d..a87edde7ba 100644 --- a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs +++ b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -1,5 +1,7 @@ -using MediaBrowser.Common.Configuration; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Playlists; using System.IO; using System.Linq; @@ -14,8 +16,15 @@ namespace MediaBrowser.Server.Implementations.Playlists public override bool IsVisible(User user) { - return GetChildren(user, true).Any() && - base.IsVisible(user); + return base.IsVisible(user) && GetRecursiveChildren(user, false) + .OfType() + .Any(i => string.Equals(i.OwnerUserId, user.Id.ToString("N"))); + } + + protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user) + { + return RecursiveChildren + .OfType(); } public override bool IsHidden @@ -48,7 +57,7 @@ namespace MediaBrowser.Server.Implementations.Playlists public BasePluginFolder GetFolder() { - var path = Path.Combine(_appPaths.DataPath, "playlists"); + var path = Path.Combine(_appPaths.CachePath, "playlists"); Directory.CreateDirectory(path); diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs index 92f01305cb..79b673283d 100644 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs @@ -1,8 +1,10 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; @@ -41,9 +43,6 @@ namespace MediaBrowser.Server.Implementations.Playlists { var name = options.Name; - // Need to use the [boxset] suffix - // If internet metadata is not found, or if xml saving is off there will be no collection.xml - // This could cause it to get re-resolved as a plain folder var folderName = _fileSystem.GetValidFilename(name) + " [playlist]"; var parentFolder = GetPlaylistsFolder(null); @@ -53,7 +52,55 @@ namespace MediaBrowser.Server.Implementations.Playlists throw new ArgumentException(); } + if (string.IsNullOrWhiteSpace(options.MediaType)) + { + foreach (var itemId in options.ItemIdList) + { + var item = _libraryManager.GetItemById(itemId); + + if (item == null) + { + throw new ArgumentException("No item exists with the supplied Id"); + } + + if (!string.IsNullOrWhiteSpace(item.MediaType)) + { + options.MediaType = item.MediaType; + } + else if (item is MusicArtist || item is MusicAlbum || item is MusicGenre) + { + options.MediaType = MediaType.Audio; + } + else if (item is Genre) + { + options.MediaType = MediaType.Video; + } + else + { + var folder = item as Folder; + if (folder != null) + { + options.MediaType = folder.GetRecursiveChildren() + .Where(i => !i.IsFolder) + .Select(i => i.MediaType) + .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); + } + } + + if (!string.IsNullOrWhiteSpace(options.MediaType)) + { + break; + } + } + } + + if (string.IsNullOrWhiteSpace(options.MediaType)) + { + throw new ArgumentException("A playlist media type is required."); + } + var path = Path.Combine(parentFolder.Path, folderName); + path = GetTargetPath(path); _iLibraryMonitor.ReportFileSystemChangeBeginning(path); @@ -61,24 +108,27 @@ namespace MediaBrowser.Server.Implementations.Playlists { Directory.CreateDirectory(path); - var collection = new Playlist + var playlist = new Playlist { Name = name, Parent = parentFolder, - Path = path + Path = path, + OwnerUserId = options.UserId }; - await parentFolder.AddChild(collection, CancellationToken.None).ConfigureAwait(false); + playlist.SetMediaType(options.MediaType); - await collection.RefreshMetadata(new MetadataRefreshOptions(), CancellationToken.None) + await parentFolder.AddChild(playlist, CancellationToken.None).ConfigureAwait(false); + + await playlist.RefreshMetadata(new MetadataRefreshOptions { ForceSave = true }, CancellationToken.None) .ConfigureAwait(false); if (options.ItemIdList.Count > 0) { - await AddToPlaylist(collection.Id.ToString("N"), options.ItemIdList); + await AddToPlaylist(playlist.Id.ToString("N"), options.ItemIdList); } - return collection; + return playlist; } finally { @@ -87,11 +137,28 @@ namespace MediaBrowser.Server.Implementations.Playlists } } + private string GetTargetPath(string path) + { + while (Directory.Exists(path)) + { + path += "1"; + } + + return path; + } + + private IEnumerable GetPlaylistItems(IEnumerable itemIds, string playlistMediaType, User user) + { + var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i != null); + + return Playlist.GetPlaylistItems(playlistMediaType, items, user); + } + public async Task AddToPlaylist(string playlistId, IEnumerable itemIds) { - var collection = _libraryManager.GetItemById(playlistId) as Playlist; + var playlist = _libraryManager.GetItemById(playlistId) as Playlist; - if (collection == null) + if (playlist == null) { throw new ArgumentException("No Playlist exists with the supplied Id"); } @@ -110,17 +177,17 @@ namespace MediaBrowser.Server.Implementations.Playlists itemList.Add(item); - list.Add(new LinkedChild - { - Type = LinkedChildType.Manual, - ItemId = item.Id - }); + list.Add(LinkedChild.Create(item)); } - collection.LinkedChildren.AddRange(list); + playlist.LinkedChildren.AddRange(list); - await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - await collection.RefreshMetadata(CancellationToken.None).ConfigureAwait(false); + await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + await playlist.RefreshMetadata(new MetadataRefreshOptions{ + + ForceSave = true + + }, CancellationToken.None).ConfigureAwait(false); } public Task RemoveFromPlaylist(string playlistId, IEnumerable indeces) diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 9235beacf5..c058193612 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -528,6 +528,7 @@ namespace MediaBrowser.WebDashboard.Api "chromecast.js", "backdrops.js", "sync.js", + "playlistmanager.js", "mediaplayer.js", "mediaplayer-video.js", @@ -621,6 +622,9 @@ namespace MediaBrowser.WebDashboard.Api "notificationsetting.js", "notificationsettings.js", "playlist.js", + "playlists.js", + "playlistedit.js", + "plugincatalogpage.js", "pluginspage.js", "remotecontrol.js", @@ -676,7 +680,6 @@ namespace MediaBrowser.WebDashboard.Api "librarymenu.css", "librarybrowser.css", "detailtable.css", - "posteritem.css", "card.css", "tileitem.css", "metadataeditor.css", diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 4bc05e0c16..c0fe0261b9 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -347,6 +347,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -527,9 +533,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -665,6 +668,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest