diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 72e5eee92f..d2574063b4 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -22,6 +22,12 @@ namespace MediaBrowser.Api.LiveTv { [ApiMember(Name = "ServiceName", Description = "Optional filter by service.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string ServiceName { get; set; } + + [ApiMember(Name = "Type", Description = "Optional filter by channel type.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public ChannelType? Type { get; set; } + + [ApiMember(Name = "UserId", Description = "Optional filter by channel user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string UserId { get; set; } } [Route("/LiveTv/Recordings", "GET")] @@ -83,24 +89,18 @@ namespace MediaBrowser.Api.LiveTv public object Get(GetChannels request) { - var result = GetChannelsAsync(request).Result; + var result = _liveTvManager.GetChannels(new ChannelQuery + { + ChannelType = request.Type, + ServiceName = request.ServiceName, + UserId = request.UserId + + }) + .Select(_liveTvManager.GetChannelInfoDto); return ToOptimizedResult(result.ToList()); } - private async Task> GetChannelsAsync(GetChannels request) - { - var services = GetServices(request.ServiceName); - - var tasks = services.Select(i => i.GetChannelsAsync(CancellationToken.None)); - - var channelLists = await Task.WhenAll(tasks).ConfigureAwait(false); - - // Aggregate all channels from all services - return channelLists.SelectMany(i => i) - .Select(_liveTvManager.GetChannelInfoDto); - } - public object Get(GetRecordings request) { var result = GetRecordingsAsync(request).Result; diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 71fa057206..6c49501826 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -1,7 +1,7 @@ -using System.Runtime.Serialization; -using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Dto; using System; using System.Collections.Generic; +using System.Runtime.Serialization; namespace MediaBrowser.Controller.Entities { diff --git a/MediaBrowser.Controller/LiveTv/Channel.cs b/MediaBrowser.Controller/LiveTv/Channel.cs new file mode 100644 index 0000000000..26245e6fb7 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/Channel.cs @@ -0,0 +1,65 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace MediaBrowser.Controller.LiveTv +{ + public class Channel : BaseItem, IItemByName + { + public Channel() + { + UserItemCounts = new Dictionary(); + } + + /// + /// Gets the user data key. + /// + /// System.String. + public override string GetUserDataKey() + { + return "Channel-" + Name; + } + + [IgnoreDataMember] + public Dictionary UserItemCounts { get; set; } + + /// + /// Gets or sets the number. + /// + /// The number. + public string ChannelNumber { get; set; } + + /// + /// Get or sets the Id. + /// + /// The id of the channel. + public string ChannelId { get; set; } + + /// + /// Gets or sets the name of the service. + /// + /// The name of the service. + public string ServiceName { get; set; } + + /// + /// Gets or sets the type of the channel. + /// + /// The type of the channel. + public ChannelType ChannelType { get; set; } + + protected override string CreateSortName() + { + double number = 0; + + if (!string.IsNullOrEmpty(ChannelNumber)) + { + double.TryParse(ChannelNumber, out number); + } + + return number.ToString("000-") + (Name ?? string.Empty); + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 62bfdf3e58..8535ac9965 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.LiveTv; using System.Collections.Generic; namespace MediaBrowser.Controller.LiveTv @@ -22,10 +21,25 @@ namespace MediaBrowser.Controller.LiveTv void AddParts(IEnumerable services); /// - /// Gets the channel info dto. + /// Gets the channels. /// - /// The info. + /// The query. + /// IEnumerable{Channel}. + IEnumerable GetChannels(ChannelQuery query); + + /// + /// Gets the channel information dto. + /// + /// The information. /// ChannelInfoDto. - ChannelInfoDto GetChannelInfoDto(ChannelInfo info); + ChannelInfoDto GetChannelInfoDto(Channel info); + + /// + /// Gets the channel. + /// + /// Name of the service. + /// The channel identifier. + /// Channel. + Channel GetChannel(string serviceName, string channelId); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index 03584240ee..56d98d5184 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -1,6 +1,6 @@ -using System; -using System.IO; +using MediaBrowser.Common.Net; using MediaBrowser.Model.LiveTv; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -50,7 +50,7 @@ namespace MediaBrowser.Controller.LiveTv /// The channel identifier. /// The cancellation token. /// Task{Stream}. - Task GetChannelImageAsync(string channelId, CancellationToken cancellationToken); + Task GetChannelImageAsync(string channelId, CancellationToken cancellationToken); /// /// Gets the recordings asynchronous. diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 393107f1e8..ea227349ea 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -104,6 +104,7 @@ + diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index ced0b712fb..6f72dc09b7 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -230,6 +230,9 @@ LiveTv\ChannelInfoDto.cs + + LiveTv\ChannelQuery.cs + LiveTv\ChannelType.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index 80b5e9ab8b..e49d0fb086 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -217,6 +217,9 @@ LiveTv\ChannelInfoDto.cs + + LiveTv\ChannelQuery.cs + LiveTv\ChannelType.cs diff --git a/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs b/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs index 8daaa75cad..b6691eca3b 100644 --- a/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs @@ -1,4 +1,5 @@ - +using System; + namespace MediaBrowser.Model.LiveTv { /// @@ -18,6 +19,12 @@ namespace MediaBrowser.Model.LiveTv /// The identifier. public string Id { get; set; } + /// + /// Gets or sets the logo image tag. + /// + /// The logo image tag. + public Guid? PrimaryImageTag { get; set; } + /// /// Gets or sets the number. /// diff --git a/MediaBrowser.Model/LiveTv/ChannelQuery.cs b/MediaBrowser.Model/LiveTv/ChannelQuery.cs new file mode 100644 index 0000000000..9fe74502fd --- /dev/null +++ b/MediaBrowser.Model/LiveTv/ChannelQuery.cs @@ -0,0 +1,27 @@ + +namespace MediaBrowser.Model.LiveTv +{ + /// + /// Class ChannelQuery. + /// + public class ChannelQuery + { + /// + /// Gets or sets the name of the service. + /// + /// The name of the service. + public string ServiceName { get; set; } + + /// + /// Gets or sets the type of the channel. + /// + /// The type of the channel. + public ChannelType? ChannelType { get; set; } + + /// + /// Gets or sets the user identifier. + /// + /// The user identifier. + public string UserId { get; set; } + } +} diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 45b5998d25..dc0571b3e1 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -61,6 +61,8 @@ + + @@ -76,7 +78,6 @@ - diff --git a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs new file mode 100644 index 0000000000..4ab6e55c8c --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs @@ -0,0 +1,100 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using System; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.LiveTv +{ + public class ChannelImageProvider : BaseMetadataProvider + { + private readonly ILiveTvManager _liveTvManager; + private readonly IProviderManager _providerManager; + + public ChannelImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager) + : base(logManager, configurationManager) + { + _liveTvManager = liveTvManager; + _providerManager = providerManager; + } + + public override bool Supports(BaseItem item) + { + return item is Channel; + } + + protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) + { + if (item.HasImage(ImageType.Primary)) + { + return false; + } + + return base.NeedsRefreshInternal(item, providerInfo); + } + + public override async Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) + { + if (item.HasImage(ImageType.Primary)) + { + SetLastRefreshed(item, DateTime.UtcNow); + return true; + } + + try + { + await DownloadImage(item, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + // Don't fail the provider on a 404 + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + + SetLastRefreshed(item, DateTime.UtcNow); + return true; + } + + private async Task DownloadImage(BaseItem item, CancellationToken cancellationToken) + { + var channel = (Channel)item; + + var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, channel.ServiceName, StringComparison.OrdinalIgnoreCase)); + + if (service != null) + { + var response = await service.GetChannelImageAsync(channel.ChannelId, cancellationToken).ConfigureAwait(false); + + // Dummy up the original url + var url = channel.ServiceName + channel.ChannelId; + + await _providerManager.SaveImage(channel, response.Content, response.ContentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false); + } + } + + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.Second; } + } + + public override ItemUpdateType ItemUpdateType + { + get + { + return ItemUpdateType.ImageUpdate; + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 05bac17c3b..9e1c6c4acb 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1,6 +1,18 @@ -using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.LiveTv { @@ -9,7 +21,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv /// public class LiveTvManager : ILiveTvManager { + private readonly IServerApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + private readonly IImageProcessor _imageProcessor; + + private List _channels = new List(); + private readonly List _services = new List(); + + public LiveTvManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + _logger = logger; + _itemRepo = itemRepo; + _imageProcessor = imageProcessor; + } + /// /// Gets the services. /// @@ -33,16 +63,147 @@ namespace MediaBrowser.Server.Implementations.LiveTv /// /// The info. /// ChannelInfoDto. - public ChannelInfoDto GetChannelInfoDto(ChannelInfo info) + public ChannelInfoDto GetChannelInfoDto(Channel info) { return new ChannelInfoDto { Name = info.Name, ServiceName = info.ServiceName, ChannelType = info.ChannelType, - Id = info.Id, - Number = info.Number + Id = info.ChannelId, + Number = info.ChannelNumber, + PrimaryImageTag = GetLogoImageTag(info) }; } + + private Guid? GetLogoImageTag(Channel info) + { + var path = info.PrimaryImagePath; + + if (string.IsNullOrEmpty(path)) + { + return null; + } + + try + { + return _imageProcessor.GetImageCacheTag(info, ImageType.Primary, path); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting channel image info for {0}", ex, info.Name); + } + + return null; + } + + public IEnumerable GetChannels(ChannelQuery query) + { + return _channels.OrderBy(i => + { + double number = 0; + + if (!string.IsNullOrEmpty(i.ChannelNumber)) + { + double.TryParse(i.ChannelNumber, out number); + } + + return number; + + }).ThenBy(i => i.Name); + } + + public Channel GetChannel(string serviceName, string channelId) + { + return _channels.FirstOrDefault(i => string.Equals(i.ServiceName, serviceName, StringComparison.OrdinalIgnoreCase) && string.Equals(i.ChannelId, channelId, StringComparison.OrdinalIgnoreCase)); + } + + internal async Task RefreshChannels(IProgress progress, CancellationToken cancellationToken) + { + // Avoid implicitly captured closure + var currentCancellationToken = cancellationToken; + + var tasks = _services.Select(i => i.GetChannelsAsync(currentCancellationToken)); + + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + var allChannels = results.SelectMany(i => i); + + var channnelTasks = allChannels.Select(i => GetChannel(i, cancellationToken)); + + var channelEntities = await Task.WhenAll(channnelTasks).ConfigureAwait(false); + + _channels = channelEntities.ToList(); + } + + private async Task GetChannel(ChannelInfo channelInfo, CancellationToken cancellationToken) + { + try + { + return await GetChannelInternal(channelInfo, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error getting channel information for {0}", ex, channelInfo.Name); + + return null; + } + } + + private async Task GetChannelInternal(ChannelInfo channelInfo, CancellationToken cancellationToken) + { + var path = Path.Combine(_appPaths.ItemsByNamePath, "channels", _fileSystem.GetValidFilename(channelInfo.ServiceName), _fileSystem.GetValidFilename(channelInfo.Name)); + + var fileInfo = new DirectoryInfo(path); + + var isNew = false; + + if (!fileInfo.Exists) + { + Directory.CreateDirectory(path); + fileInfo = new DirectoryInfo(path); + + if (!fileInfo.Exists) + { + throw new IOException("Path not created: " + path); + } + + isNew = true; + } + + var type = typeof(Channel); + + var id = (path + channelInfo.Number).GetMBId(type); + + var item = _itemRepo.RetrieveItem(id) as Channel; + + if (item == null) + { + item = new Channel + { + Name = channelInfo.Name, + Id = id, + DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo), + DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo), + Path = path, + ChannelId = channelInfo.Id, + ChannelNumber = channelInfo.Number, + ServiceName = channelInfo.ServiceName + }; + + isNew = true; + } + + // Set this now so we don't cause additional file system access during provider executions + item.ResetResolveArgs(fileInfo); + + await item.RefreshMetadata(cancellationToken, forceSave: isNew, resetResolveArgs: false); + + return item; + } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs new file mode 100644 index 0000000000..d5e10bb377 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs @@ -0,0 +1,54 @@ +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.LiveTv +{ + class RefreshChannelsScheduledTask : IScheduledTask + { + private readonly ILiveTvManager _liveTvManager; + + public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager) + { + _liveTvManager = liveTvManager; + } + + public string Name + { + get { return "Refresh Channels"; } + } + + public string Description + { + get { return "Downloads channel information from live tv services."; } + } + + public string Category + { + get { return "Live TV"; } + } + + public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress progress) + { + var manager = (LiveTvManager) _liveTvManager; + + return manager.RefreshChannels(progress, cancellationToken); + } + + public IEnumerable GetDefaultTriggers() + { + return new ITaskTrigger[] + { + + new StartupTrigger(), + + new SystemEventTrigger{ SystemEvent = SystemEvent.WakeFromSleep}, + + new IntervalTrigger{ Interval = TimeSpan.FromHours(4)} + }; + } + } +} diff --git a/MediaBrowser.Server.Implementations/Localization/Ratings/ca.txt b/MediaBrowser.Server.Implementations/Localization/Ratings/ca.txt index e551223a8c..5a110648cd 100644 --- a/MediaBrowser.Server.Implementations/Localization/Ratings/ca.txt +++ b/MediaBrowser.Server.Implementations/Localization/Ratings/ca.txt @@ -1,11 +1,6 @@ CA-G,1 -GB-U,1 CA-PG,5 -DE-0,5 CA-14A,7 -DE-12,7 CA-A,8 CA-18A,9 -SE-11,9 -DE-16,9 CA-R,10 \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 9347ea0ebe..f5ade2516c 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -167,7 +167,9 @@ + + diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 03819ff912..cd6ec18e13 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -294,7 +294,7 @@ namespace MediaBrowser.ServerApplication DtoService = new DtoService(Logger, LibraryManager, UserManager, UserDataManager, ItemRepository, ImageProcessor); RegisterSingleInstance(DtoService); - LiveTvManager = new LiveTvManager(); + LiveTvManager = new LiveTvManager(ApplicationPaths, FileSystemManager, Logger, ItemRepository, ImageProcessor); RegisterSingleInstance(LiveTvManager); var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false)); diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index 321b2f681d..75b56d1e32 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common.Internal - 3.0.245 + 3.0.246 MediaBrowser.Common.Internal Luke ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption. Copyright © Media Browser 2013 - + diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index a8b8c686ab..c24fe9dd60 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common - 3.0.245 + 3.0.246 MediaBrowser.Common Media Browser Team ebr,Luke,scottisafool diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index 1d8c165e6c..5bbed1f654 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Server.Core - 3.0.245 + 3.0.246 Media Browser.Server.Core Media Browser Team ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains core components required to build plugins for Media Browser Server. Copyright © Media Browser 2013 - +