fixes #839 - Support getting latest channel items

This commit is contained in:
Luke Pulverenti 2014-06-15 19:30:04 -04:00
parent 918034d803
commit 9e57e16aa9
11 changed files with 335 additions and 58 deletions

View File

@ -107,6 +107,53 @@ namespace MediaBrowser.Api
}
}
[Route("/Channels/Items/Latest", "GET", Summary = "Gets channel items")]
public class GetLatestChannelItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
{
/// <summary>
/// Gets or sets the user id.
/// </summary>
/// <value>The user id.</value>
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string UserId { get; set; }
/// <summary>
/// Skips over a given number of items within the results. Use for paging.
/// </summary>
/// <value>The start index.</value>
[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; }
/// <summary>
/// The maximum number of items to return
/// </summary>
/// <value>The limit.</value>
[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; }
/// <summary>
/// Gets the filters.
/// </summary>
/// <returns>IEnumerable{ItemFilter}.</returns>
public IEnumerable<ItemFilter> 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<BaseItemDto>
{
@ -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);
}
}
}

View File

@ -59,6 +59,14 @@ namespace MediaBrowser.Controller.Channels
/// <returns>Task{QueryResult{BaseItemDto}}.</returns>
Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken);
/// <summary>
/// Gets the latest media.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{QueryResult{BaseItemDto}}.</returns>
Task<QueryResult<BaseItemDto>> GetLatestChannelItems(AllChannelMediaQuery query, CancellationToken cancellationToken);
/// <summary>
/// Gets the channel items.
/// </summary>

View File

@ -63,10 +63,10 @@ namespace MediaBrowser.Model.Channels
public bool CanFilter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance can download all media.
/// Gets or sets a value indicating whether [supports content downloading].
/// </summary>
/// <value><c>true</c> if this instance can download all media; otherwise, <c>false</c>.</value>
public bool CanDownloadAllMedia { get; set; }
/// <value><c>true</c> if [supports content downloading]; otherwise, <c>false</c>.</value>
public bool SupportsContentDownloading { get; set; }
public ChannelFeatures()
{

View File

@ -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<ItemFields>();
}
public ItemFilter[] Filters { get; set; }
public List<ItemFields> Fields { get; set; }
}
}

View File

@ -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)
{

View File

@ -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<double> 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<double>();
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<double> progress)
{
if (!_config.Configuration.ChannelOptions.MaxDownloadAge.HasValue)
{
return;
}
var innerProgress = new ActionableProgress<double>();
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<double>());
}
catch (DirectoryNotFoundException)
{
// No biggie here. Nothing to delete
}
innerProgress = new ActionableProgress<double>();
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<double> progress)
private async Task DownloadLatestChannelContent(string userId,
CancellationToken cancellationToken,
IProgress<double> 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<double>();
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<double> progress)
{
var result = await _manager.GetAllMedia(new AllChannelMediaQuery
{
UserId = userId
}, cancellationToken).ConfigureAwait(false);
progress.Report(5);
var innerProgress = new ActionableProgress<double>();
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<BaseItemDto> result,
string path,
CancellationToken cancellationToken,
IProgress<double> 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<double>());
}
catch (DirectoryNotFoundException)
{
// No biggie here. Nothing to delete
}
}
/// <summary>
/// Deletes the cache files from directory with a last write time less than a given date
/// </summary>
@ -260,15 +333,22 @@ namespace MediaBrowser.Server.Implementations.Channels
}
}
/// <summary>
/// Gets a value indicating whether this instance is hidden.
/// </summary>
/// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
public bool IsHidden
{
get
{
return !_manager.GetAllChannelFeatures()
.Any(i => i.CanDownloadAllMedia && _config.Configuration.ChannelOptions.DownloadingChannels.Contains(i.Id));
return !_manager.GetAllChannelFeatures().Any();
}
}
/// <summary>
/// Gets a value indicating whether this instance is enabled.
/// </summary>
/// <value><c>true</c> if this instance is enabled; otherwise, <c>false</c>.</value>
public bool IsEnabled
{
get

View File

@ -53,6 +53,14 @@ namespace MediaBrowser.Server.Implementations.Channels
_localization = localization;
}
private TimeSpan CacheLength
{
get
{
return TimeSpan.FromDays(1);
}
}
public void AddParts(IEnumerable<IChannel> channels, IEnumerable<IChannelFactory> 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<QueryResult<BaseItemDto>> 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<IChannel, ChannelItemResult>(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<IChannel, ChannelItemResult>(i, new ChannelItemResult { });
});
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
var totalCount = results.Length;
IEnumerable<Tuple<IChannel, ChannelItemInfo>> items = results
.SelectMany(i => i.Item2.Items.Select(m => new Tuple<IChannel, ChannelItemInfo>(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<BaseItem> 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<BaseItemDto>
{
TotalRecordCount = totalCount,
Items = returnItemArray
};
}
public async Task<QueryResult<BaseItemDto>> 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<BaseItemDto>
@ -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

View File

@ -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"
}

View File

@ -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",

View File

@ -538,6 +538,7 @@ namespace MediaBrowser.WebDashboard.Api
"autoorganizetv.js",
"autoorganizelog.js",
"channels.js",
"channelslatest.js",
"channelitems.js",
"channelsettings.js",
"dashboardgeneral.js",

View File

@ -106,6 +106,9 @@
<Content Include="dashboard-ui\channelsettings.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\channelslatest.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\css\chromecast.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -622,6 +625,9 @@
<Content Include="dashboard-ui\scripts\channelsettings.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\scripts\channelslatest.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\scripts\chromecast.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>