From 1afb28b48797ee53442823cfd395e07d219e8ec3 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 22 Sep 2014 17:56:54 -0400 Subject: add cinema mode feature --- .../Channels/ChannelDownloadScheduledTask.cs | 2 +- .../Channels/ChannelManager.cs | 40 ++- .../Channels/ChannelPostScanTask.cs | 136 ++++++++ .../Connect/ConnectManager.cs | 4 +- .../Intros/DefaultIntroProvider.cs | 361 +++++++++++++++++++++ .../Library/CoreResolutionIgnoreRule.cs | 3 +- .../Library/LibraryManager.cs | 35 +- .../Library/Resolvers/LocalTrailerResolver.cs | 12 +- .../Library/Resolvers/Movies/MovieResolver.cs | 8 +- .../Localization/JavaScript/javascript.json | 3 +- .../Localization/Server/server.json | 46 ++- .../MediaBrowser.Server.Implementations.csproj | 2 + 12 files changed, 614 insertions(+), 38 deletions(-) create mode 100644 MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs create mode 100644 MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs (limited to 'MediaBrowser.Server.Implementations') diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs index 1d95dbc1bd..8c510afd27 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs @@ -332,7 +332,7 @@ namespace MediaBrowser.Server.Implementations.Channels { return new ITaskTrigger[] { - new IntervalTrigger{ Interval = TimeSpan.FromHours(6)}, + new IntervalTrigger{ Interval = TimeSpan.FromHours(3)}, }; } diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs index dfd24a248c..12aa670b38 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.Channels { get { - return TimeSpan.FromHours(12); + return TimeSpan.FromHours(6); } } @@ -663,7 +663,7 @@ namespace MediaBrowser.Server.Implementations.Channels private async Task> GetLatestItems(ISupportsLatestMedia indexable, IChannel channel, string userId, CancellationToken cancellationToken) { - var cacheLength = TimeSpan.FromHours(12); + var cacheLength = CacheLength; var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-latest", null, false); try @@ -720,7 +720,7 @@ namespace MediaBrowser.Server.Implementations.Channels } } - public async Task> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken) + public async Task> GetAllMediaInternal(AllChannelMediaQuery query, CancellationToken cancellationToken) { var user = string.IsNullOrWhiteSpace(query.UserId) ? null @@ -798,19 +798,43 @@ namespace MediaBrowser.Server.Implementations.Channels var internalItems = await Task.WhenAll(itemTasks).ConfigureAwait(false); await RefreshIfNeeded(internalItems, cancellationToken).ConfigureAwait(false); - var returnItemArray = internalItems.Select(i => _dtoService.GetBaseItemDto(i, query.Fields, user)) - .ToArray(); + var returnItemArray = internalItems.ToArray(); - return new QueryResult + return new QueryResult { TotalRecordCount = totalCount, Items = returnItemArray }; } + + public async Task> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken) + { + var user = string.IsNullOrWhiteSpace(query.UserId) + ? null + : _userManager.GetUserById(query.UserId); + + var internalResult = await GetAllMediaInternal(query, cancellationToken).ConfigureAwait(false); + + // Get everything + var fields = Enum.GetNames(typeof(ItemFields)) + .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) + .ToList(); + + var returnItems = internalResult.Items.Select(i => _dtoService.GetBaseItemDto(i, fields, user)) + .ToArray(); + + var result = new QueryResult + { + Items = returnItems, + TotalRecordCount = internalResult.TotalRecordCount + }; + + return result; + } private async Task GetAllItems(IIndexableChannel indexable, IChannel channel, string userId, CancellationToken cancellationToken) { - var cacheLength = TimeSpan.FromHours(12); + var cacheLength = CacheLength; var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-allitems", null, false); try @@ -1199,7 +1223,6 @@ namespace MediaBrowser.Server.Implementations.Channels item.Genres = info.Genres; item.Studios = info.Studios; item.CommunityRating = info.CommunityRating; - item.OfficialRating = info.OfficialRating; item.Overview = info.Overview; item.IndexNumber = info.IndexNumber; item.ParentIndexNumber = info.ParentIndexNumber; @@ -1207,6 +1230,7 @@ namespace MediaBrowser.Server.Implementations.Channels item.PremiereDate = info.PremiereDate; item.ProductionYear = info.ProductionYear; item.ProviderIds = info.ProviderIds; + item.OfficialRating = info.OfficialRating; item.DateCreated = info.DateCreated.HasValue ? info.DateCreated.Value : diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs b/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs new file mode 100644 index 0000000000..b067271c5e --- /dev/null +++ b/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Logging; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Channels +{ + public class ChannelPostScanTask : ILibraryPostScanTask + { + private readonly IChannelManager _channelManager; + private readonly IUserManager _userManager; + private readonly ILogger _logger; + + public ChannelPostScanTask(IChannelManager channelManager, IUserManager userManager, ILogger logger) + { + _channelManager = channelManager; + _userManager = userManager; + _logger = logger; + } + + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + 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 + (percentPerUser * p))); + + await DownloadContent(user, cancellationToken, innerProgress).ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= users.Count; + progress.Report(percent * 100); + } + + progress.Report(100); + } + + private async Task DownloadContent(string user, CancellationToken cancellationToken, IProgress progress) + { + var channels = await _channelManager.GetChannelsInternal(new ChannelQuery + { + UserId = user + + }, cancellationToken); + + var numComplete = 0; + + foreach (var channel in channels.Items) + { + try + { + await GetAllItems(user, channel.Id.ToString("N"), null, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting channel content", ex); + } + + numComplete++; + double percent = numComplete; + percent /= channels.Items.Length; + progress.Report(percent * 100); + } + + progress.Report(100); + + } + + private async Task GetAllItems(string user, string channelId, string folderId, bool recursive, CancellationToken cancellationToken) + { + var folderItems = new List(); + + var result = await _channelManager.GetChannelItemsInternal(new ChannelItemQuery + { + ChannelId = channelId, + UserId = user, + FolderId = folderId + + }, cancellationToken); + + folderItems.AddRange(result.Items.Where(i => i.IsFolder).Select(i => i.Id.ToString("N"))); + + var totalRetrieved = result.Items.Length; + var totalCount = result.TotalRecordCount; + + while (totalRetrieved < totalCount) + { + result = await _channelManager.GetChannelItemsInternal(new ChannelItemQuery + { + ChannelId = channelId, + UserId = user, + StartIndex = totalRetrieved, + FolderId = folderId + + }, cancellationToken); + + folderItems.AddRange(result.Items.Where(i => i.IsFolder).Select(i => i.Id.ToString("N"))); + + totalRetrieved += result.Items.Length; + totalCount = result.TotalRecordCount; + } + + if (recursive) + { + foreach (var folder in folderItems) + { + try + { + await GetAllItems(user, channelId, folder, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting channel content", ex); + } + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs index 374d04f1da..a1b88a65f5 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs +++ b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs @@ -402,11 +402,11 @@ namespace MediaBrowser.Server.Implementations.Connect } else if (!string.IsNullOrWhiteSpace(query.Name)) { - url = url + "?nameoremail=" + WebUtility.UrlEncode(query.Name); + url = url + "?name=" + WebUtility.UrlEncode(query.Name); } else if (!string.IsNullOrWhiteSpace(query.Email)) { - url = url + "?nameoremail=" + WebUtility.UrlEncode(query.Email); + url = url + "?name=" + WebUtility.UrlEncode(query.Email); } var options = new HttpRequestOptions diff --git a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs new file mode 100644 index 0000000000..b5e449eae0 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs @@ -0,0 +1,361 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Security; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Intros +{ + public class DefaultIntroProvider : IIntroProvider + { + private readonly ISecurityManager _security; + private readonly IChannelManager _channelManager; + private readonly ILocalizationManager _localization; + private readonly IConfigurationManager _serverConfig; + + public DefaultIntroProvider(ISecurityManager security, IChannelManager channelManager, ILocalizationManager localization, IConfigurationManager serverConfig) + { + _security = security; + _channelManager = channelManager; + _localization = localization; + _serverConfig = serverConfig; + } + + public async Task> GetIntros(BaseItem item, User user) + { + var config = GetOptions(); + + if (item is Movie) + { + if (!config.EnableIntrosForMovies) + { + return new List(); + } + } + else if (item is Episode) + { + if (!config.EnableIntrosForEpisodes) + { + return new List(); + } + } + else + { + return new List(); + } + + if (!IsSupporter) + { + return new List(); + } + + var ratingLevel = string.IsNullOrWhiteSpace(item.OfficialRating) + ? (int?)null + : _localization.GetRatingLevel(item.OfficialRating); + + var libaryItems = user.RootFolder.GetRecursiveChildren(user, false) + .ToList(); + + var random = new Random(Environment.TickCount + Guid.NewGuid().GetHashCode()); + + var candidates = new List(); + + if (config.EnableIntrosFromMoviesInLibrary) + { + var itemsWithTrailers = libaryItems + .Where(i => + { + var hasTrailers = i as IHasTrailers; + + if (hasTrailers != null && hasTrailers.LocalTrailerIds.Count > 0) + { + if (i is Movie) + { + return true; + } + } + return false; + }); + + candidates.AddRange(itemsWithTrailers.Select(i => new ItemWithTrailer + { + Item = i, + Type = ItemWithTrailerType.ItemWithTrailer, + User = user, + WatchingItem = item, + Random = random + })); + } + + if (config.EnableIntrosFromUpcomingTrailers) + { + var channelTrailers = await _channelManager.GetAllMediaInternal(new AllChannelMediaQuery + { + ContentTypes = new[] { ChannelMediaContentType.Trailer }, + UserId = user.Id.ToString("N") + + }, CancellationToken.None); + + candidates.AddRange(channelTrailers.Items.Select(i => new ItemWithTrailer + { + Item = i, + Type = ItemWithTrailerType.ChannelTrailer, + User = user, + WatchingItem = item, + Random = random + })); + + candidates.AddRange(libaryItems.Where(i => i is Trailer).Select(i => new ItemWithTrailer + { + Item = i, + Type = ItemWithTrailerType.LibraryTrailer, + User = user, + WatchingItem = item, + Random = random + })); + } + + var customIntros = config.EnableCustomIntro ? + GetCustomIntros(item) : + new List(); + + var trailerLimit = 2; + if (customIntros.Count > 0) + { + trailerLimit--; + } + + // Avoid implicitly captured closure + var currentUser = user; + return candidates.Where(i => + { + if (config.EnableIntrosParentalControl && !FilterByParentalRating(ratingLevel, i.Item)) + { + return false; + } + + if (!config.EnableIntrosForWatchedContent && i.IsPlayed) + { + return false; + } + return true; + }) + .OrderByDescending(i => i.Score) + .ThenBy(i => Guid.NewGuid()) + .ThenByDescending(i => (i.IsPlayed ? 0 : 1)) + .Select(i => i.IntroInfo) + .Take(trailerLimit) + .Concat(customIntros.Take(1)); + } + + private CinemaModeConfiguration GetOptions() + { + return _serverConfig.GetConfiguration("cinemamode"); + } + + private List GetCustomIntros(BaseItem item) + { + return new List(); + } + + private bool FilterByParentalRating(int? ratingLevel, BaseItem item) + { + // Only content rated same or lower + if (ratingLevel.HasValue) + { + var level = string.IsNullOrWhiteSpace(item.OfficialRating) + ? (int?)null + : _localization.GetRatingLevel(item.OfficialRating); + + return level.HasValue && level.Value <= ratingLevel.Value; + } + + return true; + } + + internal static int GetSimiliarityScore(BaseItem item1, BaseItem item2, Random random) + { + var points = 0; + + if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase)) + { + points += 10; + } + + // Find common genres + points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10); + + // Find common tags + points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10); + + // Find common keywords + points += GetKeywords(item1).Where(i => GetKeywords(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10); + + // Find common studios + points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5); + + var item2PeopleNames = item2.People.Select(i => i.Name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + + points += item1.People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i => + { + if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase)) + { + return 5; + } + if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + + return 1; + }); + + // Add some randomization so that you're not always seeing the same ones for a given movie + points += random.Next(0, 50); + + return points; + } + + private static IEnumerable GetTags(BaseItem item) + { + var hasTags = item as IHasTags; + if (hasTags != null) + { + return hasTags.Tags; + } + + return new List(); + } + + private static IEnumerable GetKeywords(BaseItem item) + { + var hasTags = item as IHasKeywords; + if (hasTags != null) + { + return hasTags.Keywords; + } + + return new List(); + } + + public IEnumerable GetAllIntroFiles() + { + return new List(); + } + + private bool IsSupporter + { + get { return _security.IsMBSupporter; } + } + + public string Name + { + get { return "Default"; } + } + + internal class ItemWithTrailer + { + internal BaseItem Item; + internal ItemWithTrailerType Type; + internal User User; + internal BaseItem WatchingItem; + internal Random Random; + + private bool? _isPlayed; + public bool IsPlayed + { + get + { + if (!_isPlayed.HasValue) + { + _isPlayed = Item.IsPlayed(User); + } + return _isPlayed.Value; + } + } + + private int? _score; + public int Score + { + get + { + if (!_score.HasValue) + { + _score = GetSimiliarityScore(WatchingItem, Item, Random); + } + return _score.Value; + } + } + + public IntroInfo IntroInfo + { + get + { + var id = Item.Id; + + if (Type == ItemWithTrailerType.ItemWithTrailer) + { + var hasTrailers = Item as IHasTrailers; + + if (hasTrailers != null) + { + id = hasTrailers.LocalTrailerIds.FirstOrDefault(); + } + } + return new IntroInfo + { + ItemId = id + }; + } + } + } + + internal enum ItemWithTrailerType + { + LibraryTrailer, + ChannelTrailer, + ItemWithTrailer + } + } + + public class CinemaModeConfigurationFactory : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new[] + { + new ConfigurationStore + { + ConfigurationType = typeof(CinemaModeConfiguration), + Key = "cinemamode" + } + }; + } + } + +} diff --git a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 7b58dd7c48..e902b939b2 100644 --- a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -95,8 +95,7 @@ namespace MediaBrowser.Server.Implementations.Library return true; } - // Don't misidentify xbmc trailers as a movie - if (filename.IndexOf(BaseItem.XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) != -1) + if (BaseItem.ExtraSuffixes.Any(i => filename.IndexOf(i.Key, StringComparison.OrdinalIgnoreCase) != -1)) { return true; } diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index ad5eac0332..6283ceb2af 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -1193,13 +1193,42 @@ namespace MediaBrowser.Server.Implementations.Library /// The item. /// The user. /// IEnumerable{System.String}. - public IEnumerable