diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index f3857ce30a..f2f0ec790c 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -70,6 +70,7 @@ + diff --git a/MediaBrowser.Api/Movies/CollectionService.cs b/MediaBrowser.Api/Movies/CollectionService.cs index 19e47eb857..e3816aa518 100644 --- a/MediaBrowser.Api/Movies/CollectionService.cs +++ b/MediaBrowser.Api/Movies/CollectionService.cs @@ -1,6 +1,7 @@ using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Collections; using MediaBrowser.Model.Querying; using ServiceStack; using System; @@ -92,9 +93,4 @@ namespace MediaBrowser.Api.Movies Task.WaitAll(task); } } - - public class CollectionCreationResult - { - public string Id { get; set; } - } } diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs new file mode 100644 index 0000000000..475183dea9 --- /dev/null +++ b/MediaBrowser.Api/PlaylistService.cs @@ -0,0 +1,84 @@ +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Playlists; +using MediaBrowser.Model.Querying; +using ServiceStack; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MediaBrowser.Api +{ + [Route("/Playlists", "POST", Summary = "Creates a new playlist")] + public class CreatePlaylist : IReturn + { + [ApiMember(Name = "Name", Description = "The name of the new playlist.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string Name { get; set; } + + [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; } + } + + [Route("/Playlists/{Id}/Items", "POST", Summary = "Adds items to a playlist")] + public class AddToPlaylist : IReturnVoid + { + [ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string Ids { get; set; } + + [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + } + + [Route("/Playlists/{Id}/Items", "DELETE", Summary = "Removes items from a playlist")] + public class RemoveFromPlaylist : IReturnVoid + { + [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Id { get; set; } + } + + [Authenticated] + public class PlaylistService : BaseApiService + { + private readonly IPlaylistManager _playlistManager; + private readonly IDtoService _dtoService; + + public PlaylistService(IDtoService dtoService, IPlaylistManager playlistManager) + { + _dtoService = dtoService; + _playlistManager = playlistManager; + } + + public object Post(CreatePlaylist request) + { + var task = _playlistManager.CreatePlaylist(new PlaylistCreationOptions + { + Name = request.Name, + ItemIdList = (request.Ids ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList() + }); + + var item = task.Result; + + var dto = _dtoService.GetBaseItemDto(item, new List()); + + return ToOptimizedResult(new PlaylistCreationResult + { + Id = dto.Id + }); + } + + public void Post(AddToPlaylist request) + { + var task = _playlistManager.AddToPlaylist(request.Id, request.Ids.Split(',')); + + Task.WaitAll(task); + } + + public void Delete(RemoveFromPlaylist request) + { + //var task = _playlistManager.RemoveFromPlaylist(request.Id, request.Ids.Split(',').Select(i => new Guid(i))); + + //Task.WaitAll(task); + } + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index aee118f9ac..b4b7b3650a 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -217,6 +217,9 @@ + + + diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs new file mode 100644 index 0000000000..2923c11c51 --- /dev/null +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -0,0 +1,47 @@ +using MediaBrowser.Controller.Entities; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Playlists +{ + public interface IPlaylistManager + { + /// + /// Gets the playlists. + /// + /// The user identifier. + /// IEnumerable<Playlist>. + IEnumerable GetPlaylists(string userId); + + /// + /// Creates the playlist. + /// + /// The options. + /// Task<Playlist>. + Task CreatePlaylist(PlaylistCreationOptions options); + + /// + /// Adds to playlist. + /// + /// The playlist identifier. + /// The item ids. + /// Task. + Task AddToPlaylist(string playlistId, IEnumerable itemIds); + + /// + /// Removes from playlist. + /// + /// The playlist identifier. + /// The indeces. + /// Task. + Task RemoveFromPlaylist(string playlistId, IEnumerable indeces); + + /// + /// Gets the playlists folder. + /// + /// The user identifier. + /// Folder. + Folder GetPlaylistsFolder(string userId); + + } +} diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs new file mode 100644 index 0000000000..e20387eba3 --- /dev/null +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -0,0 +1,20 @@ +using MediaBrowser.Controller.Entities; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Playlists +{ + public class Playlist : Folder + { + public List ItemIds { get; set; } + + public Playlist() + { + ItemIds = new List(); + } + + public override IEnumerable GetChildren(User user, bool includeLinkedChildren) + { + return base.GetChildren(user, includeLinkedChildren); + } + } +} diff --git a/MediaBrowser.Controller/Playlists/PlaylistCreationOptions.cs b/MediaBrowser.Controller/Playlists/PlaylistCreationOptions.cs new file mode 100644 index 0000000000..a62cbe12e5 --- /dev/null +++ b/MediaBrowser.Controller/Playlists/PlaylistCreationOptions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Playlists +{ + public class PlaylistCreationOptions + { + public string Name { get; set; } + + public List ItemIdList { get; set; } + + public PlaylistCreationOptions() + { + ItemIdList = new List(); + } + } +} diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index f374ed5297..ca48b88896 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -131,6 +131,9 @@ Chapters\RemoteChapterResult.cs + + Collections\CollectionCreationResult.cs + Configuration\BaseApplicationConfiguration.cs @@ -692,6 +695,9 @@ Notifications\SendToUserType.cs + + Playlists\PlaylistCreationResult.cs + Plugins\BasePluginConfiguration.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index 5385ee036c..1adf83d36a 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -94,6 +94,9 @@ Chapters\RemoteChapterResult.cs + + Collections\CollectionCreationResult.cs + Configuration\BaseApplicationConfiguration.cs @@ -649,6 +652,9 @@ Notifications\SendToUserType.cs + + Playlists\PlaylistCreationResult.cs + Plugins\BasePluginConfiguration.cs diff --git a/MediaBrowser.Model/Collections/CollectionCreationResult.cs b/MediaBrowser.Model/Collections/CollectionCreationResult.cs new file mode 100644 index 0000000000..ad7ac95535 --- /dev/null +++ b/MediaBrowser.Model/Collections/CollectionCreationResult.cs @@ -0,0 +1,8 @@ + +namespace MediaBrowser.Model.Collections +{ + public class CollectionCreationResult + { + public string Id { get; set; } + } +} diff --git a/MediaBrowser.Model/Entities/CollectionType.cs b/MediaBrowser.Model/Entities/CollectionType.cs index 5a9526d953..31cda8303e 100644 --- a/MediaBrowser.Model/Entities/CollectionType.cs +++ b/MediaBrowser.Model/Entities/CollectionType.cs @@ -23,5 +23,6 @@ public const string Games = "games"; public const string Channels = "channels"; public const string LiveTv = "livetv"; + public const string Playlists = "playlists"; } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index efd8c2a0a2..94629048c7 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -75,6 +75,7 @@ + @@ -204,6 +205,7 @@ + diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs b/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs new file mode 100644 index 0000000000..bbab8a18d2 --- /dev/null +++ b/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs @@ -0,0 +1,8 @@ + +namespace MediaBrowser.Model.Playlists +{ + public class PlaylistCreationResult + { + public string Id { get; set; } + } +} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9a26101a1f..5fb7ef11b7 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -140,7 +140,6 @@ - diff --git a/MediaBrowser.Providers/Music/SoundtrackPostScanTask.cs b/MediaBrowser.Providers/Music/SoundtrackPostScanTask.cs deleted file mode 100644 index dc94460f4e..0000000000 --- a/MediaBrowser.Providers/Music/SoundtrackPostScanTask.cs +++ /dev/null @@ -1,32 +0,0 @@ -using MediaBrowser.Controller.Library; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.Music -{ - public class SoundtrackPostScanTask : ILibraryPostScanTask - { - private readonly ILibraryManager _libraryManager; - - public SoundtrackPostScanTask(ILibraryManager libraryManager) - { - _libraryManager = libraryManager; - } - - private readonly Task _cachedTask = Task.FromResult(true); - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - RunInternal(progress, cancellationToken); - - return _cachedTask; - } - - private void RunInternal(IProgress progress, CancellationToken cancellationToken) - { - // Reimplement this when more kinds of associations are supported. - - progress.Report(100); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs index 8e51e1d7db..6a87453ab6 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs @@ -37,7 +37,7 @@ namespace MediaBrowser.Server.Implementations.Collections public Folder GetCollectionsFolder(string userId) { - return _libraryManager.RootFolder.Children.Concat(_libraryManager.RootFolder).OfType() + return _libraryManager.RootFolder.Children.OfType() .FirstOrDefault(); } diff --git a/MediaBrowser.Server.Implementations/Library/UserViewManager.cs b/MediaBrowser.Server.Implementations/Library/UserViewManager.cs index b8cb955fc9..0d54d94e8c 100644 --- a/MediaBrowser.Server.Implementations/Library/UserViewManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserViewManager.cs @@ -1,5 +1,4 @@ -using System.IO; -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; @@ -10,16 +9,17 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Server.Implementations.Configuration; namespace MediaBrowser.Server.Implementations.Library { @@ -33,8 +33,9 @@ namespace MediaBrowser.Server.Implementations.Library private readonly IChannelManager _channelManager; private readonly ILiveTvManager _liveTvManager; private readonly IServerApplicationPaths _appPaths; + private readonly IPlaylistManager _playlists; - public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IFileSystem fileSystem, IUserManager userManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerApplicationPaths appPaths) + public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IFileSystem fileSystem, IUserManager userManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerApplicationPaths appPaths, IPlaylistManager playlists) { _libraryManager = libraryManager; _localizationManager = localizationManager; @@ -43,6 +44,7 @@ namespace MediaBrowser.Server.Implementations.Library _channelManager = channelManager; _liveTvManager = liveTvManager; _appPaths = appPaths; + _playlists = playlists; } public async Task> GetUserViews(UserViewQuery query, CancellationToken cancellationToken) @@ -94,6 +96,11 @@ namespace MediaBrowser.Server.Implementations.Library list.Add(await GetUserView(CollectionType.BoxSets, user, CollectionType.BoxSets, cancellationToken).ConfigureAwait(false)); } + if (recursiveChildren.OfType().Any()) + { + list.Add(_playlists.GetPlaylistsFolder(user.Id.ToString("N"))); + } + if (query.IncludeExternalContent) { var channelResult = await _channelManager.GetChannels(new ChannelQuery diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 214d4c6c77..8bfbc08550 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -221,6 +221,8 @@ + + diff --git a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs new file mode 100644 index 0000000000..3b46191b1d --- /dev/null +++ b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -0,0 +1,62 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Server.Implementations.Playlists +{ + public class PlaylistsFolder : BasePluginFolder + { + public PlaylistsFolder() + { + Name = "Playlists"; + } + + public override bool IsVisible(User user) + { + return GetChildren(user, true).Any() && + base.IsVisible(user); + } + + public override bool IsHidden + { + get + { + return true; + } + } + + public override bool IsHiddenFromUser(User user) + { + return false; + } + + public override string CollectionType + { + get { return Model.Entities.CollectionType.Playlists; } + } + } + + public class PlaylistssDynamicFolder : IVirtualFolderCreator + { + private readonly IApplicationPaths _appPaths; + + public PlaylistssDynamicFolder(IApplicationPaths appPaths) + { + _appPaths = appPaths; + } + + public BasePluginFolder GetFolder() + { + var path = Path.Combine(_appPaths.DataPath, "playlists"); + + Directory.CreateDirectory(path); + + return new PlaylistsFolder + { + Path = path + }; + } + } +} + diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs new file mode 100644 index 0000000000..92f01305cb --- /dev/null +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs @@ -0,0 +1,137 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Playlists +{ + public class PlaylistManager : IPlaylistManager + { + private readonly ILibraryManager _libraryManager; + private readonly IFileSystem _fileSystem; + private readonly ILibraryMonitor _iLibraryMonitor; + private readonly ILogger _logger; + private readonly IUserManager _userManager; + + public PlaylistManager(ILibraryManager libraryManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILogger logger, IUserManager userManager) + { + _libraryManager = libraryManager; + _fileSystem = fileSystem; + _iLibraryMonitor = iLibraryMonitor; + _logger = logger; + _userManager = userManager; + } + + public IEnumerable GetPlaylists(string userId) + { + var user = _userManager.GetUserById(new Guid(userId)); + + return GetPlaylistsFolder(userId).GetChildren(user, true).OfType(); + } + + public async Task CreatePlaylist(PlaylistCreationOptions options) + { + 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); + + if (parentFolder == null) + { + throw new ArgumentException(); + } + + var path = Path.Combine(parentFolder.Path, folderName); + + _iLibraryMonitor.ReportFileSystemChangeBeginning(path); + + try + { + Directory.CreateDirectory(path); + + var collection = new Playlist + { + Name = name, + Parent = parentFolder, + Path = path + }; + + await parentFolder.AddChild(collection, CancellationToken.None).ConfigureAwait(false); + + await collection.RefreshMetadata(new MetadataRefreshOptions(), CancellationToken.None) + .ConfigureAwait(false); + + if (options.ItemIdList.Count > 0) + { + await AddToPlaylist(collection.Id.ToString("N"), options.ItemIdList); + } + + return collection; + } + finally + { + // Refresh handled internally + _iLibraryMonitor.ReportFileSystemChangeComplete(path, false); + } + } + + public async Task AddToPlaylist(string playlistId, IEnumerable itemIds) + { + var collection = _libraryManager.GetItemById(playlistId) as Playlist; + + if (collection == null) + { + throw new ArgumentException("No Playlist exists with the supplied Id"); + } + + var list = new List(); + var itemList = new List(); + + foreach (var itemId in itemIds) + { + var item = _libraryManager.GetItemById(itemId); + + if (item == null) + { + throw new ArgumentException("No item exists with the supplied Id"); + } + + itemList.Add(item); + + list.Add(new LinkedChild + { + Type = LinkedChildType.Manual, + ItemId = item.Id + }); + } + + collection.LinkedChildren.AddRange(list); + + await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + await collection.RefreshMetadata(CancellationToken.None).ConfigureAwait(false); + } + + public Task RemoveFromPlaylist(string playlistId, IEnumerable indeces) + { + throw new NotImplementedException(); + } + + public Folder GetPlaylistsFolder(string userId) + { + return _libraryManager.RootFolder.Children.OfType() + .FirstOrDefault(); + } + } +} diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 7dc70627bd..993cc4e1a6 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -26,6 +26,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.News; using MediaBrowser.Controller.Notifications; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; @@ -68,6 +69,7 @@ using MediaBrowser.Server.Implementations.Localization; using MediaBrowser.Server.Implementations.MediaEncoder; using MediaBrowser.Server.Implementations.Notifications; using MediaBrowser.Server.Implementations.Persistence; +using MediaBrowser.Server.Implementations.Playlists; using MediaBrowser.Server.Implementations.Security; using MediaBrowser.Server.Implementations.ServerManager; using MediaBrowser.Server.Implementations.Session; @@ -612,10 +614,13 @@ namespace MediaBrowser.ServerApplication var collectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("CollectionManager")); RegisterSingleInstance(collectionManager); + var playlistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("PlaylistManager"), UserManager); + RegisterSingleInstance(playlistManager); + LiveTvManager = new LiveTvManager(ServerConfigurationManager, FileSystemManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager); RegisterSingleInstance(LiveTvManager); - UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, FileSystemManager, UserManager, ChannelManager, LiveTvManager, ApplicationPaths); + UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, FileSystemManager, UserManager, ChannelManager, LiveTvManager, ApplicationPaths, playlistManager); RegisterSingleInstance(UserViewManager); var contentDirectory = new ContentDirectory(dlnaManager, UserDataManager, ImageProcessor, LibraryManager, ServerConfigurationManager, UserManager, LogManager.GetLogger("UpnpContentDirectory"), HttpClient, UserViewManager, ChannelManager);