diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs index 442a95f4a0..3cd032015b 100644 --- a/MediaBrowser.Api/ChannelService.cs +++ b/MediaBrowser.Api/ChannelService.cs @@ -107,6 +107,53 @@ namespace MediaBrowser.Api } } + [Route("/Channels/Items/Latest", "GET", Summary = "Gets channel items")] + public class GetLatestChannelItems : IReturn>, IHasItemFields + { + /// + /// Gets or sets the user id. + /// + /// The user id. + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string 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; } + + [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Filters { get; set; } + + [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; } + + /// + /// Gets the filters. + /// + /// IEnumerable{ItemFilter}. + public IEnumerable GetFilters() + { + var val = Filters; + + if (string.IsNullOrEmpty(val)) + { + return new ItemFilter[] { }; + } + + return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true)); + } + } + [Route("/Channels/Folder", "GET", Summary = "Gets the users channel folder, along with configured images")] public class GetChannelFolder : IReturn { @@ -173,5 +220,20 @@ namespace MediaBrowser.Api return ToOptimizedResult(result); } + + public object Get(GetLatestChannelItems request) + { + var result = _channelManager.GetLatestChannelItems(new AllChannelMediaQuery + { + Limit = request.Limit, + StartIndex = request.StartIndex, + UserId = request.UserId, + Filters = request.GetFilters().ToArray(), + Fields = request.GetItemFields().ToList() + + }, CancellationToken.None).Result; + + return ToOptimizedResult(result); + } } } diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index b6984d9ada..744eab96e0 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -59,6 +59,14 @@ namespace MediaBrowser.Controller.Channels /// Task{QueryResult{BaseItemDto}}. Task> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken); + /// + /// Gets the latest media. + /// + /// The query. + /// The cancellation token. + /// Task{QueryResult{BaseItemDto}}. + Task> GetLatestChannelItems(AllChannelMediaQuery query, CancellationToken cancellationToken); + /// /// Gets the channel items. /// diff --git a/MediaBrowser.Model/Channels/ChannelFeatures.cs b/MediaBrowser.Model/Channels/ChannelFeatures.cs index 8c41bfde75..0a919f222d 100644 --- a/MediaBrowser.Model/Channels/ChannelFeatures.cs +++ b/MediaBrowser.Model/Channels/ChannelFeatures.cs @@ -63,10 +63,10 @@ namespace MediaBrowser.Model.Channels public bool CanFilter { get; set; } /// - /// Gets or sets a value indicating whether this instance can download all media. + /// Gets or sets a value indicating whether [supports content downloading]. /// - /// true if this instance can download all media; otherwise, false. - public bool CanDownloadAllMedia { get; set; } + /// true if [supports content downloading]; otherwise, false. + public bool SupportsContentDownloading { get; set; } public ChannelFeatures() { diff --git a/MediaBrowser.Model/Channels/ChannelQuery.cs b/MediaBrowser.Model/Channels/ChannelQuery.cs index a2f428869c..9d4e26fa65 100644 --- a/MediaBrowser.Model/Channels/ChannelQuery.cs +++ b/MediaBrowser.Model/Channels/ChannelQuery.cs @@ -1,4 +1,7 @@ -namespace MediaBrowser.Model.Channels +using MediaBrowser.Model.Querying; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Channels { public class ChannelQuery { @@ -54,7 +57,13 @@ ChannelIds = new string[] { }; ContentTypes = new ChannelMediaContentType[] { }; + + Filters = new ItemFilter[] { }; + Fields = new List(); } + + public ItemFilter[] Filters { get; set; } + public List Fields { get; set; } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 6f33de67f7..1f37d3609f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -335,7 +335,7 @@ namespace MediaBrowser.Providers.Manager return false; } - if (provider is IRemoteImageProvider) + if (provider is IRemoteImageProvider || provider is IDynamicImageProvider) { if (!ConfigurationManager.Configuration.EnableInternetProviders) { diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs index b689f153cf..fdc5cfd227 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs @@ -1,6 +1,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; +using MediaBrowser.Common.Progress; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; @@ -9,6 +10,7 @@ using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; using System.IO; @@ -26,8 +28,9 @@ namespace MediaBrowser.Server.Implementations.Channels private readonly IHttpClient _httpClient; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; - public ChannelDownloadScheduledTask(IChannelManager manager, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, ILibraryManager libraryManager) + public ChannelDownloadScheduledTask(IChannelManager manager, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, ILibraryManager libraryManager, IUserManager userManager) { _manager = manager; _config = config; @@ -35,6 +38,7 @@ namespace MediaBrowser.Server.Implementations.Channels _httpClient = httpClient; _fileSystem = fileSystem; _libraryManager = libraryManager; + _userManager = userManager; } public string Name @@ -55,70 +59,118 @@ namespace MediaBrowser.Server.Implementations.Channels public async Task Execute(CancellationToken cancellationToken, IProgress progress) { CleanChannelContent(cancellationToken); - progress.Report(5); - await DownloadChannelContent(cancellationToken, progress).ConfigureAwait(false); + var users = _userManager.Users.Select(i => i.Id.ToString("N")).ToList(); + + var numComplete = 0; + + foreach (var user in users) + { + double percentPerUser = 1; + percentPerUser /= users.Count; + var startingPercent = numComplete * percentPerUser * 100; + + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => progress.Report(startingPercent + (.8 * p))); + + await DownloadContent(user, cancellationToken, innerProgress).ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= users.Count; + progress.Report(percent * 100); + } + progress.Report(100); } - private void CleanChannelContent(CancellationToken cancellationToken) + private async Task DownloadContent(string user, + CancellationToken cancellationToken, + IProgress progress) { - if (!_config.Configuration.ChannelOptions.MaxDownloadAge.HasValue) - { - return; - } + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => progress.Report(0 + (.8 * p))); + await DownloadAllChannelContent(user, cancellationToken, innerProgress).ConfigureAwait(false); + progress.Report(80); - var minDateModified = DateTime.UtcNow.AddDays(0 - _config.Configuration.ChannelOptions.MaxDownloadAge.Value); - - var path = _manager.ChannelDownloadPath; - - try - { - DeleteCacheFilesFromDirectory(cancellationToken, path, minDateModified, new Progress()); - } - catch (DirectoryNotFoundException) - { - // No biggie here. Nothing to delete - } + innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => progress.Report(80 + (.2 * p))); + await DownloadLatestChannelContent(user, cancellationToken, progress).ConfigureAwait(false); + progress.Report(100); } - private async Task DownloadChannelContent(CancellationToken cancellationToken, IProgress progress) + private async Task DownloadLatestChannelContent(string userId, + CancellationToken cancellationToken, + IProgress progress) { - if (_config.Configuration.ChannelOptions.DownloadingChannels.Length == 0) + var result = await _manager.GetLatestChannelItems(new AllChannelMediaQuery { - return; - } - - var result = await _manager.GetAllMedia(new AllChannelMediaQuery - { - ChannelIds = _config.Configuration.ChannelOptions.DownloadingChannels + UserId = userId }, cancellationToken).ConfigureAwait(false); + progress.Report(5); + + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => progress.Report(5 + (.95 * p))); + var path = _manager.ChannelDownloadPath; + await DownloadChannelContent(result, path, cancellationToken, innerProgress).ConfigureAwait(false); + } + + private async Task DownloadAllChannelContent(string userId, + CancellationToken cancellationToken, + IProgress progress) + { + var result = await _manager.GetAllMedia(new AllChannelMediaQuery + { + UserId = userId + + }, cancellationToken).ConfigureAwait(false); + + progress.Report(5); + + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => progress.Report(5 + (.95 * p))); + + var path = _manager.ChannelDownloadPath; + + await DownloadChannelContent(result, path, cancellationToken, innerProgress).ConfigureAwait(false); + } + + private async Task DownloadChannelContent(QueryResult result, + string path, + CancellationToken cancellationToken, + IProgress progress) + { var numComplete = 0; foreach (var item in result.Items) { - try + if (_config.Configuration.ChannelOptions.DownloadingChannels.Contains(item.ChannelId)) { - await DownloadChannelItem(item, cancellationToken, path); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - _logger.ErrorException("Error downloading channel content for {0}", ex, item.Name); + try + { + await DownloadChannelItem(item, cancellationToken, path); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.ErrorException("Error downloading channel content for {0}", ex, item.Name); + } } numComplete++; double percent = numComplete; percent /= result.Items.Length; - progress.Report(percent * 95 + 5); + progress.Report(percent * 100); } + + progress.Report(100); } private async Task DownloadChannelItem(BaseItemDto item, @@ -212,6 +264,27 @@ namespace MediaBrowser.Server.Implementations.Channels }; } + private void CleanChannelContent(CancellationToken cancellationToken) + { + if (!_config.Configuration.ChannelOptions.MaxDownloadAge.HasValue) + { + return; + } + + var minDateModified = DateTime.UtcNow.AddDays(0 - _config.Configuration.ChannelOptions.MaxDownloadAge.Value); + + var path = _manager.ChannelDownloadPath; + + try + { + DeleteCacheFilesFromDirectory(cancellationToken, path, minDateModified, new Progress()); + } + catch (DirectoryNotFoundException) + { + // No biggie here. Nothing to delete + } + } + /// /// Deletes the cache files from directory with a last write time less than a given date /// @@ -260,15 +333,22 @@ namespace MediaBrowser.Server.Implementations.Channels } } + /// + /// Gets a value indicating whether this instance is hidden. + /// + /// true if this instance is hidden; otherwise, false. public bool IsHidden { get { - return !_manager.GetAllChannelFeatures() - .Any(i => i.CanDownloadAllMedia && _config.Configuration.ChannelOptions.DownloadingChannels.Contains(i.Id)); + return !_manager.GetAllChannelFeatures().Any(); } } + /// + /// Gets a value indicating whether this instance is enabled. + /// + /// true if this instance is enabled; otherwise, false. public bool IsEnabled { get diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs index 553c683fd0..ad775b5764 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs @@ -53,6 +53,14 @@ namespace MediaBrowser.Server.Implementations.Channels _localization = localization; } + private TimeSpan CacheLength + { + get + { + return TimeSpan.FromDays(1); + } + } + public void AddParts(IEnumerable channels, IEnumerable factories) { _channels = channels.ToArray(); @@ -443,6 +451,7 @@ namespace MediaBrowser.Server.Implementations.Channels InternalChannelFeatures features) { var isIndexable = provider is IIndexableChannel; + var supportsLatest = provider is ISupportsLatestMedia; return new ChannelFeatures { @@ -453,10 +462,10 @@ namespace MediaBrowser.Server.Implementations.Channels MaxPageSize = features.MaxPageSize, MediaTypes = features.MediaTypes, SupportsSortOrderToggle = features.SupportsSortOrderToggle, - SupportsLatestMedia = provider is ISupportsLatestMedia, + SupportsLatestMedia = supportsLatest, Name = channel.Name, Id = channel.Id.ToString("N"), - CanDownloadAllMedia = isIndexable + SupportsContentDownloading = isIndexable || supportsLatest }; } @@ -470,6 +479,105 @@ namespace MediaBrowser.Server.Implementations.Channels return ("Channel " + name).GetMBId(typeof(Channel)); } + public async Task> GetLatestChannelItems(AllChannelMediaQuery query, CancellationToken cancellationToken) + { + var user = string.IsNullOrWhiteSpace(query.UserId) + ? null + : _userManager.GetUserById(new Guid(query.UserId)); + + var channels = _channels; + + if (query.ChannelIds.Length > 0) + { + // Avoid implicitly captured closure + var ids = query.ChannelIds; + channels = channels + .Where(i => ids.Contains(GetInternalChannelId(i.Name).ToString("N"))) + .ToArray(); + } + + // Avoid implicitly captured closure + var userId = query.UserId; + + var tasks = channels + .Select(async i => + { + var indexable = i as ISupportsLatestMedia; + + if (indexable != null) + { + try + { + var result = await indexable.GetLatestMedia(new ChannelLatestMediaSearch + { + UserId = userId + + }, cancellationToken).ConfigureAwait(false); + + var resultItems = result.ToList(); + + return new Tuple(i, new ChannelItemResult + { + Items = resultItems, + TotalRecordCount = resultItems.Count + }); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting all media from {0}", ex, i.Name); + } + } + return new Tuple(i, new ChannelItemResult { }); + }); + + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + var totalCount = results.Length; + + IEnumerable> items = results + .SelectMany(i => i.Item2.Items.Select(m => new Tuple(i.Item1, m))) + .OrderBy(i => i.Item2.Name); + + if (query.ContentTypes.Length > 0) + { + // Avoid implicitly captured closure + var contentTypes = query.ContentTypes; + + items = items.Where(i => contentTypes.Contains(i.Item2.ContentType)); + } + + // Avoid implicitly captured closure + var token = cancellationToken; + var itemTasks = items.Select(i => + { + var channelProvider = i.Item1; + var channel = GetChannel(GetInternalChannelId(channelProvider.Name).ToString("N")); + return GetChannelItemEntity(i.Item2, channelProvider, channel, token); + }); + + IEnumerable internalItems = await Task.WhenAll(itemTasks).ConfigureAwait(false); + + internalItems = ApplyFilters(internalItems, query.Filters, user); + + if (query.StartIndex.HasValue) + { + internalItems = internalItems.Skip(query.StartIndex.Value); + } + if (query.Limit.HasValue) + { + internalItems = internalItems.Take(query.Limit.Value); + } + + var returnItemArray = internalItems.Select(i => _dtoService.GetBaseItemDto(i, query.Fields, user)) + .ToArray(); + + return new QueryResult + { + TotalRecordCount = totalCount, + Items = returnItemArray + }; + } + public async Task> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken) { var user = string.IsNullOrWhiteSpace(query.UserId) @@ -480,11 +588,16 @@ namespace MediaBrowser.Server.Implementations.Channels if (query.ChannelIds.Length > 0) { + // Avoid implicitly captured closure + var ids = query.ChannelIds; channels = channels - .Where(i => query.ChannelIds.Contains(GetInternalChannelId(i.Name).ToString("N"))) + .Where(i => ids.Contains(GetInternalChannelId(i.Name).ToString("N"))) .ToArray(); } + // Avoid implicitly captured closure + var userId = query.UserId; + var tasks = channels .Select(async i => { @@ -496,7 +609,7 @@ namespace MediaBrowser.Server.Implementations.Channels { var result = await indexable.GetAllMedia(new InternalAllChannelMediaQuery { - UserId = query.UserId + UserId = userId }, cancellationToken).ConfigureAwait(false); @@ -546,12 +659,7 @@ namespace MediaBrowser.Server.Implementations.Channels var internalItems = await Task.WhenAll(itemTasks).ConfigureAwait(false); - // Get everything - var fields = Enum.GetNames(typeof(ItemFields)) - .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) - .ToList(); - - var returnItemArray = internalItems.Select(i => _dtoService.GetBaseItemDto(i, fields, user)) + var returnItemArray = internalItems.Select(i => _dtoService.GetBaseItemDto(i, query.Fields, user)) .ToArray(); return new QueryResult @@ -641,7 +749,7 @@ namespace MediaBrowser.Server.Implementations.Channels { var userId = user.Id.ToString("N"); - var cacheLength = TimeSpan.FromDays(1); + var cacheLength = CacheLength; var cachePath = GetChannelDataCachePath(channel, userId, folderId, sortField, sortDescending); try diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json index 74c1791bab..d4c31a61e3 100644 --- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -147,5 +147,6 @@ "ButtonRemove": "Remove", "LabelChapterDownloaders": "Chapter downloaders:", "LabelChapterDownloadersHelp": "Enable and rank your preferred chapter downloaders in order of priority. Lower priority downloaders will only be used to fill in missing information.", - "HeaderFavoriteAlbums": "Favorite Albums" + "HeaderFavoriteAlbums": "Favorite Albums", + "HeaderLatestChannelMedia": "Latest Channel Items" } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json index 5626238c14..af6a312e0a 100644 --- a/MediaBrowser.Server.Implementations/Localization/Server/server.json +++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json @@ -785,11 +785,13 @@ "LabelHomePageSection1": "Home page section one:", "LabelHomePageSection2": "Home page section two:", "LabelHomePageSection3": "Home page section three:", + "LabelHomePageSection4": "Home page section four:", "OptionMyLibraryButtons": "My library (buttons)", "OptionMyLibrary": "My library", "OptionMyLibrarySmall": "My library (small)", "OptionResumablemedia": "Resume", "OptionLatestMedia": "Latest media", + "OptionLatestChannelMedia": "Latest channel items", "OptionNone": "None", "HeaderLiveTv": "Live TV", "HeaderReports": "Reports", diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 78344965d5..aaa9300551 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -538,6 +538,7 @@ namespace MediaBrowser.WebDashboard.Api "autoorganizetv.js", "autoorganizelog.js", "channels.js", + "channelslatest.js", "channelitems.js", "channelsettings.js", "dashboardgeneral.js", diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 709340dc00..1a2bd8ef15 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -106,6 +106,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -622,6 +625,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest