diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations/Channels/ChannelManager.cs')
| -rw-r--r-- | MediaBrowser.Server.Implementations/Channels/ChannelManager.cs | 546 |
1 files changed, 396 insertions, 150 deletions
diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs index dfd24a248..d88625396 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs @@ -1,5 +1,8 @@ -using MediaBrowser.Common.Extensions; +using System.Net; +using System.Text; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -12,6 +15,8 @@ using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using System; @@ -21,6 +26,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Server.Implementations.Library; namespace MediaBrowser.Server.Implementations.Channels { @@ -37,13 +43,17 @@ namespace MediaBrowser.Server.Implementations.Channels private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClient _httpClient; private readonly ILocalizationManager _localization; private readonly ConcurrentDictionary<Guid, bool> _refreshedItems = new ConcurrentDictionary<Guid, bool>(); + private readonly ConcurrentDictionary<string, int> _downloadCounts = new ConcurrentDictionary<string, int>(); + private Timer _refreshTimer; + private Timer _clearDownloadCountsTimer; - public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, ILocalizationManager localization) + public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, ILocalizationManager localization, IHttpClient httpClient) { _userManager = userManager; _dtoService = dtoService; @@ -54,15 +64,17 @@ namespace MediaBrowser.Server.Implementations.Channels _userDataManager = userDataManager; _jsonSerializer = jsonSerializer; _localization = localization; + _httpClient = httpClient; _refreshTimer = new Timer(s => _refreshedItems.Clear(), null, TimeSpan.FromHours(3), TimeSpan.FromHours(3)); + _clearDownloadCountsTimer = new Timer(s => _downloadCounts.Clear(), null, TimeSpan.FromHours(24), TimeSpan.FromHours(24)); } private TimeSpan CacheLength { get { - return TimeSpan.FromHours(12); + return TimeSpan.FromHours(6); } } @@ -167,12 +179,9 @@ namespace MediaBrowser.Server.Implementations.Channels var internalResult = await GetChannelsInternal(query, cancellationToken).ConfigureAwait(false); - // Get everything - var fields = Enum.GetNames(typeof(ItemFields)) - .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) - .ToList(); + var dtoOptions = new DtoOptions(); - var returnItems = internalResult.Items.Select(i => _dtoService.GetBaseItemDto(i, fields, user)) + var returnItems = internalResult.Items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) .ToArray(); var result = new QueryResult<BaseItemDto> @@ -233,7 +242,7 @@ namespace MediaBrowser.Server.Implementations.Channels return item; } - public async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSources(string id, CancellationToken cancellationToken) + public async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSources(string id, bool includeDynamicSources, CancellationToken cancellationToken) { var item = (IChannelMediaItem)_libraryManager.GetItemById(id); @@ -244,7 +253,7 @@ namespace MediaBrowser.Server.Implementations.Channels IEnumerable<ChannelMediaInfo> results; - if (requiresCallback != null) + if (requiresCallback != null && includeDynamicSources) { results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken) .ConfigureAwait(false); @@ -261,7 +270,7 @@ namespace MediaBrowser.Server.Implementations.Channels sources.InsertRange(0, cachedVersions); - return sources; + return sources.Where(IsValidMediaSource); } private readonly ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>> _channelItemMediaInfo = @@ -307,13 +316,13 @@ namespace MediaBrowser.Server.Implementations.Channels if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { - files = files.Where(i => EntityResolutionHelper.IsVideoFile(i.FullName)); + files = files.Where(i => _libraryManager.IsVideoFile(i.FullName)); } else { - files = files.Where(i => EntityResolutionHelper.IsAudioFile(i.FullName)); + files = files.Where(i => _libraryManager.IsAudioFile(i.FullName)); } - + var file = files .FirstOrDefault(i => i.Name.StartsWith(filenamePrefix, StringComparison.OrdinalIgnoreCase)); @@ -348,59 +357,13 @@ namespace MediaBrowser.Server.Implementations.Channels private MediaSourceInfo GetMediaSource(IChannelMediaItem item, ChannelMediaInfo info) { - var id = info.Path.GetMD5().ToString("N"); + var source = info.ToMediaSource(); - var source = new MediaSourceInfo - { - MediaStreams = GetMediaStreams(info).ToList(), - - Container = info.Container, - Protocol = info.Protocol, - Path = info.Path, - RequiredHttpHeaders = info.RequiredHttpHeaders, - RunTimeTicks = item.RunTimeTicks, - Name = id, - Id = id - }; + source.RunTimeTicks = source.RunTimeTicks ?? item.RunTimeTicks; return source; } - private IEnumerable<MediaStream> GetMediaStreams(ChannelMediaInfo info) - { - var list = new List<MediaStream>(); - - if (!string.IsNullOrWhiteSpace(info.VideoCodec) && - !string.IsNullOrWhiteSpace(info.AudioCodec)) - { - list.Add(new MediaStream - { - Type = MediaStreamType.Video, - Width = info.Width, - RealFrameRate = info.Framerate, - Profile = info.VideoProfile, - Level = info.VideoLevel, - Index = -1, - Height = info.Height, - Codec = info.VideoCodec, - BitRate = info.VideoBitrate, - AverageFrameRate = info.Framerate - }); - - list.Add(new MediaStream - { - Type = MediaStreamType.Audio, - Index = -1, - Codec = info.AudioCodec, - BitRate = info.AudioBitrate, - Channels = info.AudioChannels, - SampleRate = info.AudioSampleRate - }); - } - - return list; - } - private IEnumerable<ChannelMediaInfo> SortMediaInfoResults(IEnumerable<ChannelMediaInfo> channelMediaSources) { var list = channelMediaSources.ToList(); @@ -522,7 +485,6 @@ namespace MediaBrowser.Server.Implementations.Channels public ChannelFeatures GetChannelFeatures(string id) { var channel = GetChannel(id); - var channelProvider = GetChannelProvider(channel); return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures()); @@ -547,7 +509,8 @@ namespace MediaBrowser.Server.Implementations.Channels SupportsLatestMedia = supportsLatest, Name = channel.Name, Id = channel.Id.ToString("N"), - SupportsContentDownloading = isIndexable || supportsLatest + SupportsContentDownloading = isIndexable || supportsLatest, + AutoRefreshLevels = features.AutoRefreshLevels }; } @@ -567,6 +530,51 @@ namespace MediaBrowser.Server.Implementations.Channels ? null : _userManager.GetUserById(query.UserId); + var limit = query.Limit; + + // See below about parental control + if (user != null) + { + query.StartIndex = null; + query.Limit = null; + } + + var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false); + + var items = internalResult.Items; + var totalRecordCount = internalResult.TotalRecordCount; + + // Supporting parental control is a hack because it has to be done after querying the remote data source + // This will get screwy if apps try to page, so limit to 10 results in an attempt to always keep them on the first page + if (user != null) + { + items = items.Where(i => i.IsVisible(user)) + .Take(limit ?? 10) + .ToArray(); + + totalRecordCount = items.Length; + } + + var dtoOptions = new DtoOptions(); + + var returnItems = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) + .ToArray(); + + var result = new QueryResult<BaseItemDto> + { + Items = returnItems, + TotalRecordCount = totalRecordCount + }; + + return result; + } + + public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(AllChannelMediaQuery query, CancellationToken cancellationToken) + { + var user = string.IsNullOrWhiteSpace(query.UserId) + ? null + : _userManager.GetUserById(query.UserId); + if (!string.IsNullOrWhiteSpace(query.UserId) && user == null) { throw new ArgumentException("User not found."); @@ -627,6 +635,13 @@ namespace MediaBrowser.Server.Implementations.Channels items = items.Where(i => contentTypes.Contains(i.Item2.ContentType)); } + if (query.ExtraTypes.Length > 0) + { + // Avoid implicitly captured closure + var contentTypes = query.ExtraTypes; + + items = items.Where(i => contentTypes.Contains(i.Item2.ExtraType)); + } // Avoid implicitly captured closure var token = cancellationToken; @@ -640,7 +655,7 @@ namespace MediaBrowser.Server.Implementations.Channels var internalItems = await Task.WhenAll(itemTasks).ConfigureAwait(false); internalItems = ApplyFilters(internalItems, query.Filters, user).ToArray(); - await RefreshIfNeeded(internalItems, cancellationToken).ConfigureAwait(false); + await RefreshIfNeeded(internalItems, new Progress<double>(), cancellationToken).ConfigureAwait(false); if (query.StartIndex.HasValue) { @@ -651,10 +666,9 @@ namespace MediaBrowser.Server.Implementations.Channels internalItems = internalItems.Take(query.Limit.Value).ToArray(); } - var returnItemArray = internalItems.Select(i => _dtoService.GetBaseItemDto(i, query.Fields, user)) - .ToArray(); + var returnItemArray = internalItems.ToArray(); - return new QueryResult<BaseItemDto> + return new QueryResult<BaseItem> { TotalRecordCount = totalCount, Items = returnItemArray @@ -663,7 +677,7 @@ namespace MediaBrowser.Server.Implementations.Channels private async Task<IEnumerable<ChannelItemInfo>> 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,12 +734,8 @@ namespace MediaBrowser.Server.Implementations.Channels } } - public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItem>> GetAllMediaInternal(AllChannelMediaQuery query, CancellationToken cancellationToken) { - var user = string.IsNullOrWhiteSpace(query.UserId) - ? null - : _userManager.GetUserById(query.UserId); - var channels = GetAllChannels(); if (query.ChannelIds.Length > 0) @@ -737,9 +747,6 @@ namespace MediaBrowser.Server.Implementations.Channels .ToArray(); } - // Avoid implicitly captured closure - var userId = query.UserId; - var tasks = channels .Select(async i => { @@ -749,7 +756,14 @@ namespace MediaBrowser.Server.Implementations.Channels { try { - var result = await GetAllItems(indexable, i, userId, cancellationToken).ConfigureAwait(false); + var result = await GetAllItems(indexable, i, new InternalAllChannelMediaQuery + { + UserId = query.UserId, + ContentTypes = query.ContentTypes, + ExtraTypes = query.ExtraTypes, + TrailerTypes = query.TrailerTypes + + }, cancellationToken).ConfigureAwait(false); return new Tuple<IChannel, ChannelItemResult>(i, result); } @@ -769,14 +783,6 @@ namespace MediaBrowser.Server.Implementations.Channels .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)); - } - if (query.StartIndex.HasValue) { items = items.Skip(query.StartIndex.Value); @@ -796,22 +802,45 @@ 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<BaseItemDto> + return new QueryResult<BaseItem> { TotalRecordCount = totalCount, Items = returnItemArray }; } - private async Task<ChannelItemResult> GetAllItems(IIndexableChannel indexable, IChannel channel, string userId, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken) + { + var user = string.IsNullOrWhiteSpace(query.UserId) + ? null + : _userManager.GetUserById(query.UserId); + + var internalResult = await GetAllMediaInternal(query, cancellationToken).ConfigureAwait(false); + + await RefreshIfNeeded(internalResult.Items, new Progress<double>(), cancellationToken).ConfigureAwait(false); + + var dtoOptions = new DtoOptions(); + + var returnItems = internalResult.Items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) + .ToArray(); + + var result = new QueryResult<BaseItemDto> + { + Items = returnItems, + TotalRecordCount = internalResult.TotalRecordCount + }; + + return result; + } + + private async Task<ChannelItemResult> GetAllItems(IIndexableChannel indexable, IChannel channel, InternalAllChannelMediaQuery query, CancellationToken cancellationToken) { - var cacheLength = TimeSpan.FromHours(12); - var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-allitems", null, false); + var cacheLength = CacheLength; + var folderId = _jsonSerializer.SerializeToString(query).GetMD5().ToString("N"); + var cachePath = GetChannelDataCachePath(channel, query.UserId, folderId, null, false); try { @@ -849,11 +878,7 @@ namespace MediaBrowser.Server.Implementations.Channels } - var result = await indexable.GetAllMedia(new InternalAllChannelMediaQuery - { - UserId = userId - - }, cancellationToken).ConfigureAwait(false); + var result = await indexable.GetAllMedia(query, cancellationToken).ConfigureAwait(false); CacheResponse(result, cachePath); @@ -865,7 +890,7 @@ namespace MediaBrowser.Server.Implementations.Channels } } - public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(ChannelItemQuery query, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(ChannelItemQuery query, IProgress<double> progress, CancellationToken cancellationToken) { // Get the internal channel entity var channel = GetChannel(query.ChannelId); @@ -935,7 +960,7 @@ namespace MediaBrowser.Server.Implementations.Channels } } - return await GetReturnItems(internalItems, providerTotalRecordCount, user, query, cancellationToken).ConfigureAwait(false); + return await GetReturnItems(internalItems, providerTotalRecordCount, user, query, progress, cancellationToken).ConfigureAwait(false); } public async Task<QueryResult<BaseItemDto>> GetChannelItems(ChannelItemQuery query, CancellationToken cancellationToken) @@ -944,14 +969,11 @@ namespace MediaBrowser.Server.Implementations.Channels ? null : _userManager.GetUserById(query.UserId); - var internalResult = await GetChannelItemsInternal(query, cancellationToken).ConfigureAwait(false); + var internalResult = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).ConfigureAwait(false); - // Get everything - var fields = Enum.GetNames(typeof(ItemFields)) - .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) - .ToList(); + var dtoOptions = new DtoOptions(); - var returnItems = internalResult.Items.Select(i => _dtoService.GetBaseItemDto(i, fields, user)) + var returnItems = internalResult.Items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) .ToArray(); var result = new QueryResult<BaseItemDto> @@ -1104,7 +1126,12 @@ namespace MediaBrowser.Server.Implementations.Channels filename + ".json"); } - private async Task<QueryResult<BaseItem>> GetReturnItems(IEnumerable<BaseItem> items, int? totalCountFromProvider, User user, ChannelItemQuery query, CancellationToken cancellationToken) + private async Task<QueryResult<BaseItem>> GetReturnItems(IEnumerable<BaseItem> items, + int? totalCountFromProvider, + User user, + ChannelItemQuery query, + IProgress<double> progress, + CancellationToken cancellationToken) { items = ApplyFilters(items, query.Filters, user); @@ -1126,9 +1153,8 @@ namespace MediaBrowser.Server.Implementations.Channels } } - await RefreshIfNeeded(all, cancellationToken).ConfigureAwait(false); - var returnItemArray = all.ToArray(); + await RefreshIfNeeded(returnItemArray, progress, cancellationToken).ConfigureAwait(false); return new QueryResult<BaseItem> { @@ -1137,60 +1163,62 @@ namespace MediaBrowser.Server.Implementations.Channels }; } - private string GetIdToHash(string externalId, IChannel channelProvider) + private string GetIdToHash(string externalId, string channelName) { // Increment this as needed to force new downloads // Incorporate Name because it's being used to convert channel entity to provider - return externalId + (channelProvider.DataVersion ?? string.Empty) + - (channelProvider.Name ?? string.Empty) + "16"; + return externalId + (channelName ?? string.Empty) + "16"; } - private async Task<BaseItem> GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, CancellationToken cancellationToken) + private T GetItemById<T>(string idString, string channelName, string channnelDataVersion, out bool isNew) + where T : BaseItem, IChannelItem, new() { - BaseItem item; - Guid id; - var isNew = false; + var id = GetIdToHash(idString, channelName).GetMBId(typeof(T)); - var idToHash = GetIdToHash(info.Id, channelProvider); + T item = null; - if (info.Type == ChannelItemType.Folder) + try { - id = idToHash.GetMBId(typeof(ChannelFolderItem)); - - item = _libraryManager.GetItemById(id) as ChannelFolderItem; - - if (item == null) - { - isNew = true; - item = new ChannelFolderItem(); - } + item = _libraryManager.GetItemById(id) as T; } - else if (info.MediaType == ChannelMediaType.Audio) + catch (Exception ex) { - id = idToHash.GetMBId(typeof(ChannelAudioItem)); - - item = _libraryManager.GetItemById(id) as ChannelAudioItem; + _logger.ErrorException("Error retrieving channel item from database", ex); + } - if (item == null) - { - isNew = true; - item = new ChannelAudioItem(); - } + if (item == null || !string.Equals(item.DataVersion, channnelDataVersion, StringComparison.Ordinal)) + { + item = new T(); + isNew = true; } else { - id = idToHash.GetMBId(typeof(ChannelVideoItem)); + isNew = false; + } + + item.DataVersion = channnelDataVersion; + item.Id = id; + return item; + } - item = _libraryManager.GetItemById(id) as ChannelVideoItem; + private async Task<BaseItem> GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, CancellationToken cancellationToken) + { + BaseItem item; + bool isNew; - if (item == null) - { - isNew = true; - item = new ChannelVideoItem(); - } + if (info.Type == ChannelItemType.Folder) + { + item = GetItemById<ChannelFolderItem>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + } + else if (info.MediaType == ChannelMediaType.Audio) + { + item = GetItemById<ChannelAudioItem>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + } + else + { + item = GetItemById<ChannelVideoItem>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); } - item.Id = id; item.RunTimeTicks = info.RunTimeTicks; if (isNew) @@ -1199,7 +1227,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 +1234,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 : @@ -1230,6 +1258,7 @@ namespace MediaBrowser.Server.Implementations.Channels if (channelMediaItem != null) { channelMediaItem.ContentType = info.ContentType; + channelMediaItem.ExtraType = info.ExtraType; channelMediaItem.ChannelMediaSources = info.MediaSources; var mediaSource = info.MediaSources.FirstOrDefault(); @@ -1248,11 +1277,19 @@ namespace MediaBrowser.Server.Implementations.Channels return item; } - private async Task RefreshIfNeeded(IEnumerable<BaseItem> programs, CancellationToken cancellationToken) + private async Task RefreshIfNeeded(BaseItem[] programs, IProgress<double> progress, CancellationToken cancellationToken) { + var numComplete = 0; + var numItems = programs.Length; + foreach (var program in programs) { await RefreshIfNeeded(program, cancellationToken).ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); } } @@ -1358,12 +1395,9 @@ namespace MediaBrowser.Server.Implementations.Channels { var user = string.IsNullOrEmpty(userId) ? null : _userManager.GetUserById(userId); - // Get everything - var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList(); - var folder = await GetInternalChannelFolder(userId, cancellationToken).ConfigureAwait(false); - return _dtoService.GetBaseItemDto(folder, fields, user); + return _dtoService.GetBaseItemDto(folder, new DtoOptions(), user); } public async Task<Folder> GetInternalChannelFolder(string userId, CancellationToken cancellationToken) @@ -1372,8 +1406,220 @@ namespace MediaBrowser.Server.Implementations.Channels return await _libraryManager.GetNamedView(name, "channels", "zz_" + name, cancellationToken).ConfigureAwait(false); } + public async Task DownloadChannelItem(IChannelMediaItem item, string destination, + IProgress<double> progress, CancellationToken cancellationToken) + { + var itemId = item.Id.ToString("N"); + var sources = await GetChannelItemMediaSources(itemId, true, cancellationToken) + .ConfigureAwait(false); + + var list = sources.Where(i => i.Protocol == MediaProtocol.Http).ToList(); + + foreach (var source in list) + { + try + { + await TryDownloadChannelItem(source, item, destination, progress, cancellationToken).ConfigureAwait(false); + return; + } + catch (HttpException ex) + { + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + MarkBadMediaSource(source); + } + } + } + } + + private async Task TryDownloadChannelItem(MediaSourceInfo source, + IChannelMediaItem item, + string destination, + IProgress<double> progress, + CancellationToken cancellationToken) + { + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = source.Path, + Progress = new Progress<double>() + }; + + var host = new Uri(source.Path).Host.ToLower(); + var channel = GetChannel(item.ChannelId); + var channelProvider = GetChannelProvider(channel); + var limit = channelProvider.GetChannelFeatures().DailyDownloadLimit; + + if (!ValidateDownloadLimit(host, limit)) + { + _logger.Error(string.Format("Download limit has been reached for {0}", channel.Name)); + throw new ChannelDownloadException(string.Format("Download limit has been reached for {0}", channel.Name)); + } + + foreach (var header in source.RequiredHttpHeaders) + { + options.RequestHeaders[header.Key] = header.Value; + } + + Directory.CreateDirectory(Path.GetDirectoryName(destination)); + + // Determine output extension + var response = await _httpClient.GetTempFileResponse(options).ConfigureAwait(false); + + if (response.ContentType.StartsWith("text/html")) + { + throw new HttpException("File not found") + { + StatusCode = HttpStatusCode.NotFound + }; + } + + IncrementDownloadCount(host, limit); + + if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase) && response.ContentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) + { + var extension = response.ContentType.Split('/') + .Last() + .Replace("quicktime", "mov", StringComparison.OrdinalIgnoreCase); + + destination += "." + extension; + } + else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) && response.ContentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase)) + { + var extension = response.ContentType.Replace("audio/mpeg", "audio/mp3", StringComparison.OrdinalIgnoreCase) + .Split('/') + .Last(); + + destination += "." + extension; + } + else + { + File.Delete(response.TempFilePath); + + throw new ApplicationException("Unexpected response type encountered: " + response.ContentType); + } + + File.Copy(response.TempFilePath, destination, true); + + try + { + File.Delete(response.TempFilePath); + } + catch + { + + } + } + + private readonly ReaderWriterLockSlim _mediaSourceHistoryLock = new ReaderWriterLockSlim(); + private bool IsValidMediaSource(MediaSourceInfo source) + { + if (source.Protocol == MediaProtocol.Http) + { + return !GetBadMediaSourceHistory().Contains(source.Path, StringComparer.OrdinalIgnoreCase); + } + return true; + } + + private void MarkBadMediaSource(MediaSourceInfo source) + { + var list = GetBadMediaSourceHistory(); + list.Add(source.Path); + + var path = GetMediaSourceHistoryPath(); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + if (_mediaSourceHistoryLock.TryEnterWriteLock(TimeSpan.FromSeconds(5))) + { + try + { + File.WriteAllLines(path, list.ToArray(), Encoding.UTF8); + } + catch (Exception ex) + { + _logger.ErrorException("Error saving file", ex); + } + finally + { + _mediaSourceHistoryLock.ExitWriteLock(); + } + } + } + + private ConcurrentBag<string> _badMediaSources = null; + private ConcurrentBag<string> GetBadMediaSourceHistory() + { + if (_badMediaSources == null) + { + var path = GetMediaSourceHistoryPath(); + + if (_mediaSourceHistoryLock.TryEnterReadLock(TimeSpan.FromSeconds(1))) + { + if (_badMediaSources == null) + { + try + { + _badMediaSources = new ConcurrentBag<string>(File.ReadAllLines(path, Encoding.UTF8)); + } + catch (IOException) + { + _badMediaSources = new ConcurrentBag<string>(); + } + catch (Exception ex) + { + _logger.ErrorException("Error reading file", ex); + _badMediaSources = new ConcurrentBag<string>(); + } + finally + { + _mediaSourceHistoryLock.ExitReadLock(); + } + } + } + } + return _badMediaSources; + } + + private string GetMediaSourceHistoryPath() + { + return Path.Combine(_config.ApplicationPaths.DataPath, "channels", "failures.txt"); + } + + private void IncrementDownloadCount(string key, int? limit) + { + if (!limit.HasValue) + { + return; + } + + int current; + _downloadCounts.TryGetValue(key, out current); + + current++; + _downloadCounts.AddOrUpdate(key, current, (k, v) => current); + } + + private bool ValidateDownloadLimit(string key, int? limit) + { + if (!limit.HasValue) + { + return true; + } + + int current; + _downloadCounts.TryGetValue(key, out current); + + return current < limit.Value; + } + public void Dispose() { + if (_clearDownloadCountsTimer != null) + { + _clearDownloadCountsTimer.Dispose(); + _clearDownloadCountsTimer = null; + } if (_refreshTimer != null) { _refreshTimer.Dispose(); |
