Browse Source

add channels infrastructure

Luke Pulverenti 11 years ago
parent
commit
6936336b82

+ 63 - 1
MediaBrowser.Api/ChannelService.cs

@@ -1,8 +1,12 @@
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
+using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
 
 namespace MediaBrowser.Api
@@ -10,10 +14,25 @@ namespace MediaBrowser.Api
     [Route("/Channels", "GET", Summary = "Gets available channels")]
     public class GetChannels : IReturn<QueryResult<BaseItemDto>>
     {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "path", 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; }
     }
 
@@ -24,11 +43,51 @@ namespace MediaBrowser.Api
 
         public string CategoryId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "path", 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 = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public SortOrder? SortOrder { get; set; }
+
+        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsRecentlyAdded, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string Filters { get; set; }
+
+        [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        public string SortBy { 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));
+        }
     }
 
     public class ChannelService : BaseApiService
@@ -61,7 +120,10 @@ namespace MediaBrowser.Api
                 StartIndex = request.StartIndex,
                 UserId = request.UserId,
                 ChannelId = request.Id,
-                CategoryId = request.CategoryId
+                CategoryId = request.CategoryId,
+                SortOrder = request.SortOrder,
+                SortBy = (request.SortBy ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(),
+                Filters = request.GetFilters().ToArray()
 
             }, CancellationToken.None).Result;
 

+ 12 - 0
MediaBrowser.Controller/Channels/Channel.cs

@@ -1,9 +1,21 @@
 using MediaBrowser.Controller.Entities;
+using System;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Channels
 {
     public class Channel : BaseItem
     {
         public string OriginalChannelName { get; set; }
+
+        public override bool IsVisible(User user)
+        {
+            if (user.Configuration.BlockedChannels.Contains(Name, StringComparer.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+            
+            return base.IsVisible(user);
+        }
     }
 }

+ 16 - 1
MediaBrowser.Controller/Channels/ChannelAudioItem.cs

@@ -1,4 +1,6 @@
-using MediaBrowser.Controller.Entities.Audio;
+using System.Linq;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Configuration;
 
 namespace MediaBrowser.Controller.Channels
 {
@@ -13,5 +15,18 @@ namespace MediaBrowser.Controller.Channels
         public ChannelMediaContentType ContentType { get; set; }
 
         public string OriginalImageUrl { get; set; }
+
+        protected override bool GetBlockUnratedValue(UserConfiguration config)
+        {
+            return config.BlockUnratedItems.Contains(UnratedItem.ChannelContent);
+        }
+
+        public override bool SupportsLocalMetadata
+        {
+            get
+            {
+                return false;
+            }
+        }
     }
 }

+ 15 - 0
MediaBrowser.Controller/Channels/ChannelCategoryItem.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
 
 namespace MediaBrowser.Controller.Channels
 {
@@ -9,5 +10,19 @@ namespace MediaBrowser.Controller.Channels
         public ChannelItemType ChannelItemType { get; set; }
 
         public string OriginalImageUrl { get; set; }
+
+        protected override bool GetBlockUnratedValue(UserConfiguration config)
+        {
+            // Don't block. 
+            return false;
+        }
+
+        public override bool SupportsLocalMetadata
+        {
+            get
+            {
+                return false;
+            }
+        }
     }
 }

+ 22 - 1
MediaBrowser.Controller/Channels/ChannelItemInfo.cs

@@ -18,6 +18,7 @@ namespace MediaBrowser.Controller.Channels
         public string Overview { get; set; }
 
         public List<string> Genres { get; set; }
+        public List<string> Studios { get; set; }
 
         public List<PersonInfo> People { get; set; }
         
@@ -38,9 +39,15 @@ namespace MediaBrowser.Controller.Channels
         public DateTime? PremiereDate { get; set; }
         public int? ProductionYear { get; set; }
 
+        public DateTime? DateCreated { get; set; }
+        
+        public List<ChannelMediaInfo> MediaSources { get; set; }
+        
         public ChannelItemInfo()
         {
+            MediaSources = new List<ChannelMediaInfo>();
             Genres = new List<string>();
+            Studios = new List<string>();
             People = new List<PersonInfo>();
             ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         }
@@ -70,6 +77,20 @@ namespace MediaBrowser.Controller.Channels
 
         Movie = 3,
 
-        Episode = 4
+        Episode = 4,
+
+        Song = 5
+    }
+
+    public class ChannelMediaInfo
+    {
+        public string Path { get; set; }
+
+        public Dictionary<string, string> RequiredHttpHeaders { get; set; }
+
+        public ChannelMediaInfo()
+        {
+            RequiredHttpHeaders = new Dictionary<string, string>();
+        }
     }
 }

+ 40 - 0
MediaBrowser.Controller/Channels/ChannelVideoItem.cs

@@ -1,4 +1,8 @@
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System.Globalization;
+using System.Linq;
 
 namespace MediaBrowser.Controller.Channels
 {
@@ -13,5 +17,41 @@ namespace MediaBrowser.Controller.Channels
         public ChannelMediaContentType ContentType { get; set; }
 
         public string OriginalImageUrl { get; set; }
+
+        public override string GetUserDataKey()
+        {
+            if (ContentType == ChannelMediaContentType.Trailer)
+            {
+                var key = this.GetProviderId(MetadataProviders.Tmdb) ?? this.GetProviderId(MetadataProviders.Tvdb) ?? this.GetProviderId(MetadataProviders.Imdb) ?? this.GetProviderId(MetadataProviders.Tvcom);
+
+                if (!string.IsNullOrWhiteSpace(key))
+                {
+                    key = key + "-trailer";
+
+                    // Make sure different trailers have their own data.
+                    if (RunTimeTicks.HasValue)
+                    {
+                        key += "-" + RunTimeTicks.Value.ToString(CultureInfo.InvariantCulture);
+                    }
+
+                    return key;
+                }
+            }
+
+            return base.GetUserDataKey();
+        }
+
+        protected override bool GetBlockUnratedValue(UserConfiguration config)
+        {
+            return config.BlockUnratedItems.Contains(UnratedItem.ChannelContent);
+        }
+
+        public override bool SupportsLocalMetadata
+        {
+            get
+            {
+                return false;
+            }
+        }
     }
 }

+ 24 - 10
MediaBrowser.Controller/Channels/IChannel.cs

@@ -17,16 +17,10 @@ namespace MediaBrowser.Controller.Channels
         string Name { get; }
 
         /// <summary>
-        /// Gets the home page URL.
-        /// </summary>
-        /// <value>The home page URL.</value>
-        string HomePageUrl { get; }
-
-        /// <summary>
-        /// Gets the capabilities.
+        /// Gets the channel information.
         /// </summary>
-        /// <returns>ChannelCapabilities.</returns>
-        ChannelCapabilities GetCapabilities();
+        /// <returns>ChannelInfo.</returns>
+        ChannelInfo GetChannelInfo();
 
         /// <summary>
         /// Determines whether [is enabled for] [the specified user].
@@ -67,9 +61,29 @@ namespace MediaBrowser.Controller.Channels
         IEnumerable<ImageType> GetSupportedChannelImages();
     }
 
-    public class ChannelCapabilities
+    public class ChannelInfo
     {
+        /// <summary>
+        /// Gets the home page URL.
+        /// </summary>
+        /// <value>The home page URL.</value>
+        public string HomePageUrl { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance can search.
+        /// </summary>
+        /// <value><c>true</c> if this instance can search; otherwise, <c>false</c>.</value>
         public bool CanSearch { get; set; }
+
+        public List<ChannelMediaType> MediaTypes { get; set; }
+
+        public List<ChannelMediaContentType> ContentTypes { get; set; }
+
+        public ChannelInfo()
+        {
+            MediaTypes = new List<ChannelMediaType>();
+            ContentTypes = new List<ChannelMediaContentType>();
+        }
     }
 
     public class ChannelSearchInfo

+ 1 - 0
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -9,6 +9,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Querying;
 
 namespace MediaBrowser.Controller.Library
 {

+ 14 - 2
MediaBrowser.Model/Channels/ChannelQuery.cs

@@ -1,4 +1,6 @@
-
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+
 namespace MediaBrowser.Model.Channels
 {
     public class ChannelQuery
@@ -35,7 +37,7 @@ namespace MediaBrowser.Model.Channels
         /// </summary>
         /// <value>The category identifier.</value>
         public string CategoryId { get; set; }
-        
+
         /// <summary>
         /// Gets or sets the user identifier.
         /// </summary>
@@ -53,5 +55,15 @@ namespace MediaBrowser.Model.Channels
         /// </summary>
         /// <value>The limit.</value>
         public int? Limit { get; set; }
+
+        public SortOrder? SortOrder { get; set; }
+        public string[] SortBy { get; set; }
+        public ItemFilter[] Filters { get; set; }
+
+        public ChannelItemQuery()
+        {
+            Filters = new ItemFilter[] { };
+            SortBy = new string[] { };
+        }
     }
 }

+ 3 - 0
MediaBrowser.Model/Configuration/UserConfiguration.cs

@@ -57,6 +57,7 @@ namespace MediaBrowser.Model.Configuration
         public bool GroupMoviesIntoBoxSets { get; set; }
 
         public string[] BlockedMediaFolders { get; set; }
+        public string[] BlockedChannels { get; set; }
 
         public UnratedItem[] BlockUnratedItems { get; set; }
 
@@ -74,6 +75,7 @@ namespace MediaBrowser.Model.Configuration
             GroupMoviesIntoBoxSets = true;
 
             BlockedMediaFolders = new string[] { };
+            BlockedChannels = new string[] { };
             BlockUnratedItems = new UnratedItem[] { };
         }
     }
@@ -88,6 +90,7 @@ namespace MediaBrowser.Model.Configuration
         Book,
         LiveTvChannel,
         LiveTvProgram,
+        ChannelContent,
         Other
     }
 }

+ 211 - 36
MediaBrowser.Server.Implementations/Channels/ChannelManager.cs

@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Querying;
 using System;
@@ -25,13 +26,14 @@ namespace MediaBrowser.Server.Implementations.Channels
         private List<Channel> _channelEntities = new List<Channel>();
 
         private readonly IUserManager _userManager;
+        private readonly IUserDataManager _userDataManager;
         private readonly IDtoService _dtoService;
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
 
-        public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem)
+        public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager)
         {
             _userManager = userManager;
             _dtoService = dtoService;
@@ -39,6 +41,7 @@ namespace MediaBrowser.Server.Implementations.Channels
             _logger = logger;
             _config = config;
             _fileSystem = fileSystem;
+            _userDataManager = userDataManager;
         }
 
         public void AddParts(IEnumerable<IChannel> channels)
@@ -158,7 +161,9 @@ namespace MediaBrowser.Server.Implementations.Channels
                 isNew = true;
             }
 
-            item.HomePageUrl = channelInfo.HomePageUrl;
+            var info = channelInfo.GetChannelInfo();
+
+            item.HomePageUrl = info.HomePageUrl;
             item.OriginalChannelName = channelInfo.Name;
 
             if (string.IsNullOrEmpty(item.Name))
@@ -198,8 +203,7 @@ namespace MediaBrowser.Server.Implementations.Channels
             var items = await GetChannelItems(channelProvider, user, query.CategoryId, cancellationToken)
                         .ConfigureAwait(false);
 
-
-            return await GetReturnItems(items, user, query.StartIndex, query.Limit, cancellationToken).ConfigureAwait(false);
+            return await GetReturnItems(items, user, query, cancellationToken).ConfigureAwait(false);
         }
 
         private async Task<IEnumerable<ChannelItemInfo>> GetChannelItems(IChannel channel, User user, string categoryId, CancellationToken cancellationToken)
@@ -217,76 +221,247 @@ namespace MediaBrowser.Server.Implementations.Channels
             return result.Items;
         }
 
-        private async Task<QueryResult<BaseItemDto>> GetReturnItems(IEnumerable<ChannelItemInfo> items, User user, int? startIndex, int? limit, CancellationToken cancellationToken)
+        private async Task<QueryResult<BaseItemDto>> GetReturnItems(IEnumerable<ChannelItemInfo> items, User user, ChannelItemQuery query, CancellationToken cancellationToken)
         {
-            if (startIndex.HasValue)
-            {
-                items = items.Skip(startIndex.Value);
-            }
-            if (limit.HasValue)
-            {
-                items = items.Take(limit.Value);
-            }
-
             // Get everything
             var fields = Enum.GetNames(typeof(ItemFields))
                     .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
                     .ToList();
 
-            var tasks = items.Select(GetChannelItemEntity);
+            var tasks = items.Select(i => GetChannelItemEntity(i, cancellationToken));
+
+            IEnumerable<BaseItem> entities = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            entities = ApplyFilters(entities, query.Filters, user);
 
-            var returnItems = await Task.WhenAll(tasks).ConfigureAwait(false);
-            returnItems = new BaseItem[] {};
-            var returnItemArray = returnItems.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
+            entities = _libraryManager.Sort(entities, user, query.SortBy, query.SortOrder ?? SortOrder.Ascending);
+
+            var all = entities.ToList();
+            var totalCount = all.Count;
+
+            if (query.StartIndex.HasValue)
+            {
+                all = all.Skip(query.StartIndex.Value).ToList();
+            }
+            if (query.Limit.HasValue)
+            {
+                all = all.Take(query.Limit.Value).ToList();
+            }
+
+            await RefreshIfNeeded(all, cancellationToken).ConfigureAwait(false);
+
+            var returnItemArray = all.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
                 .ToArray();
 
             return new QueryResult<BaseItemDto>
             {
                 Items = returnItemArray,
-                TotalRecordCount = returnItems.Length
+                TotalRecordCount = totalCount
             };
         }
 
-        private async Task<BaseItem> GetChannelItemEntity(ChannelItemInfo info)
+        private string GetIdToHash(string externalId)
         {
-            BaseItem item;
+            // Increment this as needed to force new downloads
+            return externalId + "3";
+        }
 
+        private async Task<BaseItem> GetChannelItemEntity(ChannelItemInfo info, CancellationToken cancellationToken)
+        {
+            BaseItem item;
             Guid id;
+            var isNew = false;
 
             if (info.Type == ChannelItemType.Category)
             {
-                id = info.Id.GetMBId(typeof(ChannelCategoryItem));
-                item = new ChannelCategoryItem();
+                id = GetIdToHash(info.Id).GetMBId(typeof(ChannelCategoryItem));
+
+                item = _libraryManager.GetItemById(id) as ChannelCategoryItem;
+
+                if (item == null)
+                {
+                    isNew = true;
+                    item = new ChannelCategoryItem();
+                }
             }
             else if (info.MediaType == ChannelMediaType.Audio)
             {
-                id = info.Id.GetMBId(typeof(ChannelCategoryItem));
-                item = new ChannelAudioItem();
+                id = GetIdToHash(info.Id).GetMBId(typeof(ChannelCategoryItem));
+
+                item = _libraryManager.GetItemById(id) as ChannelAudioItem;
+
+                if (item == null)
+                {
+                    isNew = true;
+                    item = new ChannelAudioItem();
+                }
             }
             else
             {
-                id = info.Id.GetMBId(typeof(ChannelVideoItem));
-                item = new ChannelVideoItem();
+                id = GetIdToHash(info.Id).GetMBId(typeof(ChannelVideoItem));
+
+                item = _libraryManager.GetItemById(id) as ChannelVideoItem;
+
+                if (item == null)
+                {
+                    isNew = true;
+                    item = new ChannelVideoItem();
+                }
             }
 
             item.Id = id;
-            item.Name = info.Name;
-            item.Genres = info.Genres;
-            item.CommunityRating = info.CommunityRating;
-            item.OfficialRating = info.OfficialRating;
-            item.Overview = info.Overview;
-            item.People = info.People;
-            item.PremiereDate = info.PremiereDate;
-            item.ProductionYear = info.ProductionYear;
             item.RunTimeTicks = info.RunTimeTicks;
-            item.ProviderIds = info.ProviderIds;
+
+            var mediaSource = info.MediaSources.FirstOrDefault();
+
+            item.Path = mediaSource == null ? null : mediaSource.Path;
+
+            if (isNew)
+            {
+                item.Name = info.Name;
+                item.Genres = info.Genres;
+                item.Studios = info.Studios;
+                item.CommunityRating = info.CommunityRating;
+                item.OfficialRating = info.OfficialRating;
+                item.Overview = info.Overview;
+                item.People = info.People;
+                item.PremiereDate = info.PremiereDate;
+                item.ProductionYear = info.ProductionYear;
+                item.ProviderIds = info.ProviderIds;
+
+                if (info.DateCreated.HasValue)
+                {
+                    item.DateCreated = info.DateCreated.Value;
+                }
+            }
+
+            var channelItem = (IChannelItem)item;
+
+            channelItem.OriginalImageUrl = info.ImageUrl;
+            channelItem.ExternalId = info.Id;
+            channelItem.ChannelItemType = info.Type;
+
+            var channelMediaItem = item as IChannelMediaItem;
+
+            if (channelMediaItem != null)
+            {
+                channelMediaItem.IsInfiniteStream = info.IsInfiniteStream;
+                channelMediaItem.ContentType = info.ContentType;
+            }
+
+            if (isNew)
+            {
+                await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
+                _libraryManager.RegisterItem(item);
+            }
 
             return item;
         }
 
+        private async Task RefreshIfNeeded(IEnumerable<BaseItem> programs, CancellationToken cancellationToken)
+        {
+            foreach (var program in programs)
+            {
+                await RefreshIfNeeded(program, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        private async Task RefreshIfNeeded(BaseItem program, CancellationToken cancellationToken)
+        {
+            //if (_refreshedPrograms.ContainsKey(program.Id))
+            {
+                //return;
+            }
+
+            await program.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+
+            //_refreshedPrograms.TryAdd(program.Id, true);
+        }
+
         internal IChannel GetChannelProvider(Channel channel)
         {
             return _channels.First(i => string.Equals(i.Name, channel.OriginalChannelName, StringComparison.OrdinalIgnoreCase));
         }
+
+        private IEnumerable<BaseItem> ApplyFilters(IEnumerable<BaseItem> items, IEnumerable<ItemFilter> filters, User user)
+        {
+            foreach (var filter in filters.OrderByDescending(f => (int)f))
+            {
+                items = ApplyFilter(items, filter, user);
+            }
+
+            return items;
+        }
+
+        private IEnumerable<BaseItem> ApplyFilter(IEnumerable<BaseItem> items, ItemFilter filter, User user)
+        {
+            // Avoid implicitly captured closure
+            var currentUser = user;
+
+            switch (filter)
+            {
+                case ItemFilter.IsFavoriteOrLikes:
+                    return items.Where(item =>
+                    {
+                        var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey());
+
+                        if (userdata == null)
+                        {
+                            return false;
+                        }
+
+                        var likes = userdata.Likes ?? false;
+                        var favorite = userdata.IsFavorite;
+
+                        return likes || favorite;
+                    });
+
+                case ItemFilter.Likes:
+                    return items.Where(item =>
+                    {
+                        var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey());
+
+                        return userdata != null && userdata.Likes.HasValue && userdata.Likes.Value;
+                    });
+
+                case ItemFilter.Dislikes:
+                    return items.Where(item =>
+                    {
+                        var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey());
+
+                        return userdata != null && userdata.Likes.HasValue && !userdata.Likes.Value;
+                    });
+
+                case ItemFilter.IsFavorite:
+                    return items.Where(item =>
+                    {
+                        var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey());
+
+                        return userdata != null && userdata.IsFavorite;
+                    });
+
+                case ItemFilter.IsResumable:
+                    return items.Where(item =>
+                    {
+                        var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey());
+
+                        return userdata != null && userdata.PlaybackPositionTicks > 0;
+                    });
+
+                case ItemFilter.IsPlayed:
+                    return items.Where(item => item.IsPlayed(currentUser));
+
+                case ItemFilter.IsUnplayed:
+                    return items.Where(item => item.IsUnplayed(currentUser));
+
+                case ItemFilter.IsFolder:
+                    return items.Where(item => item.IsFolder);
+
+                case ItemFilter.IsNotFolder:
+                    return items.Where(item => !item.IsFolder);
+            }
+
+            return items;
+        }
     }
 }

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -228,7 +228,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 return;
             }
 
-            await program.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+            await program.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 
             _refreshedPrograms.TryAdd(program.Id, true);
         }

+ 2 - 1
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -76,6 +76,7 @@
 	"LabelMaxParentalRating": "Maximum allowed parental rating:",
 	"MaxParentalRatingHelp": "Content with a higher rating will be hidden from this user.",
 	"LibraryAccessHelp": "Select the media folders to share with this user. Administrators will be able to edit all folders using the metadata manager.",
+	"ChannelAccessHelp": "Select the channels to share with this user. Administrators will be able to edit all channels using the metadata manager.",
 	"ButtonDeleteImage": "Delete Image",
 	"LabelSelectUsers": "Select users:",
 	"ButtonUpload": "Upload",
@@ -267,7 +268,7 @@
 	"LabelAutomaticUpdatesTmdbHelp": "If enabled, new images will be downloaded automatically as they're added to TheMovieDB.org. Existing images will not be replaced.",
 	"LabelAutomaticUpdatesTvdbHelp": "If enabled, new images will be downloaded automatically as they're added to TheTVDB.com. Existing images will not be replaced.",
 	"ExtractChapterImagesHelp": "Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task at 4am, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours.",
-	"LabelMetadataDownloadLanguage": "Preferred language:",
+	"LabelMetadataDownloadLanguage": "Preferred download language:",
 	"ButtonAutoScroll": "Auto-scroll",
 	"LabelImageSavingConvention": "Image saving convention:",
 	"LabelImageSavingConventionHelp": "Media Browser recognizes images from most major media applications. Choosing your downloading convention is useful if you also use other products.",

+ 1 - 1
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -507,7 +507,7 @@ namespace MediaBrowser.ServerApplication
                 MediaEncoder);
             RegisterSingleInstance(EncodingManager);
 
-            ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, Logger, ServerConfigurationManager, FileSystemManager);
+            ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, Logger, ServerConfigurationManager, FileSystemManager, UserDataManager);
             RegisterSingleInstance(ChannelManager);
 
             var appThemeManager = new AppThemeManager(ApplicationPaths, FileSystemManager, JsonSerializer, Logger);

+ 1 - 0
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -658,6 +658,7 @@ namespace MediaBrowser.WebDashboard.Api
                                       "contextmenu.css",
                                       "mediaplayer.css",
                                       "mediaplayer-video.css",
+                                      "librarymenu.css",
                                       "librarybrowser.css",
                                       "detailtable.css",
                                       "posteritem.css",

+ 6 - 1
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -163,7 +163,9 @@
     <Content Include="dashboard-ui\css\images\clients\xbmc.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\css\images\favicon.ico" />
+    <Content Include="dashboard-ui\css\images\favicon.ico">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\css\images\icons\audiocd.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -221,6 +223,9 @@
     <Content Include="dashboard-ui\css\images\media\tvflyout.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\css\librarymenu.css">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\css\livetv.css">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.358</version>
+        <version>3.0.360</version>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.358" />
+            <dependency id="MediaBrowser.Common" version="3.0.360" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.4.1" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common</id>
-        <version>3.0.358</version>
+        <version>3.0.360</version>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.358</version>
+        <version>3.0.360</version>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.358" />
+            <dependency id="MediaBrowser.Common" version="3.0.360" />
         </dependencies>
     </metadata>
     <files>