aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations
diff options
context:
space:
mode:
authorLuke Pulverenti <luke.pulverenti@gmail.com>2014-06-02 15:32:41 -0400
committerLuke Pulverenti <luke.pulverenti@gmail.com>2014-06-02 15:32:41 -0400
commit858c37b8607ff0698a94b9e7bfff6190d3bca56d (patch)
treeec673c5ebe7ffe813b6a16340471ac472a5dbf5b /MediaBrowser.Server.Implementations
parent36648d27082c1ee50c1483e17f14ba1ae838a00e (diff)
add channel downloading settings
Diffstat (limited to 'MediaBrowser.Server.Implementations')
-rw-r--r--MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs277
-rw-r--r--MediaBrowser.Server.Implementations/Channels/ChannelManager.cs264
-rw-r--r--MediaBrowser.Server.Implementations/Dto/DtoService.cs33
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs53
-rw-r--r--MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json4
-rw-r--r--MediaBrowser.Server.Implementations/Localization/Server/server.json12
-rw-r--r--MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj1
-rw-r--r--MediaBrowser.Server.Implementations/Session/HttpSessionController.cs21
8 files changed, 616 insertions, 49 deletions
diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs
new file mode 100644
index 000000000..21b5ba6da
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs
@@ -0,0 +1,277 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+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.Channels
+{
+ public class ChannelDownloadScheduledTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ private readonly IChannelManager _manager;
+ private readonly IServerConfigurationManager _config;
+ private readonly ILogger _logger;
+ private readonly IHttpClient _httpClient;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+
+ public ChannelDownloadScheduledTask(IChannelManager manager, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, ILibraryManager libraryManager)
+ {
+ _manager = manager;
+ _config = config;
+ _logger = logger;
+ _httpClient = httpClient;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ }
+
+ public string Name
+ {
+ get { return "Download channel content"; }
+ }
+
+ public string Description
+ {
+ get { return "Downloads channel content based on configuration."; }
+ }
+
+ public string Category
+ {
+ get { return "Channels"; }
+ }
+
+ public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ CleanChannelContent(cancellationToken);
+ progress.Report(5);
+
+ await DownloadChannelContent(cancellationToken, progress).ConfigureAwait(false);
+ progress.Report(100);
+ }
+
+ private void CleanChannelContent(CancellationToken cancellationToken)
+ {
+ if (!_config.Configuration.ChannelOptions.MaxDownloadAge.HasValue)
+ {
+ return;
+ }
+
+ var minDateModified = DateTime.UtcNow.AddDays(0 - _config.Configuration.ChannelOptions.MaxDownloadAge.Value);
+
+ var path = _manager.ChannelDownloadPath;
+
+ try
+ {
+ DeleteCacheFilesFromDirectory(cancellationToken, path, minDateModified, new Progress<double>());
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // No biggie here. Nothing to delete
+ }
+ }
+
+ private async Task DownloadChannelContent(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ if (_config.Configuration.ChannelOptions.DownloadingChannels.Length == 0)
+ {
+ return;
+ }
+
+ var result = await _manager.GetAllMedia(new AllChannelMediaQuery
+ {
+ ChannelIds = _config.Configuration.ChannelOptions.DownloadingChannels
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ var path = _manager.ChannelDownloadPath;
+
+ var numComplete = 0;
+
+ foreach (var item in result.Items)
+ {
+ try
+ {
+ await DownloadChannelItem(item, cancellationToken, path);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error downloading channel content for {0}", ex, item.Name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= result.Items.Length;
+ progress.Report(percent * 95 + 5);
+ }
+ }
+
+ private async Task DownloadChannelItem(BaseItemDto item,
+ CancellationToken cancellationToken,
+ string path)
+ {
+ var sources = await _manager.GetChannelItemMediaSources(item.Id, cancellationToken)
+ .ConfigureAwait(false);
+
+ var list = sources.ToList();
+
+ var cachedVersions = list.Where(i => i.LocationType == LocationType.FileSystem).ToList();
+
+ if (cachedVersions.Count > 0)
+ {
+ await RefreshMediaSourceItems(cachedVersions, item.IsVideo, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var source = list.First();
+
+ var options = new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = source.Path,
+ Progress = new Progress<double>()
+ };
+
+ foreach (var header in source.RequiredHttpHeaders)
+ {
+ options.RequestHeaders[header.Key] = header.Value;
+ }
+
+ var destination = Path.Combine(path, item.ChannelId, source.Path.GetMD5().ToString("N"));
+ Directory.CreateDirectory(Path.GetDirectoryName(destination));
+
+ // Determine output extension
+ var response = await _httpClient.GetTempFileResponse(options).ConfigureAwait(false);
+
+ if (item.IsVideo && response.ContentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
+ {
+ var extension = response.ContentType.Split('/')
+ .Last();
+
+ destination += "." + extension;
+ }
+ else if (item.IsAudio && response.ContentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase))
+ {
+ var extension = response.ContentType.Replace("audio/mpeg", "audio/mp3", StringComparison.OrdinalIgnoreCase)
+ .Split('/')
+ .Last();
+
+ destination += "." + extension;
+ }
+ else
+ {
+ throw new ApplicationException("Unexpected response type encountered: " + response.ContentType);
+ }
+
+ File.Move(response.TempFilePath, destination);
+
+ await RefreshMediaSourceItem(destination, item.IsVideo, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task RefreshMediaSourceItems(IEnumerable<MediaSourceInfo> items, bool isVideo, CancellationToken cancellationToken)
+ {
+ foreach (var item in items)
+ {
+ await RefreshMediaSourceItem(item.Path, isVideo, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task RefreshMediaSourceItem(string path, bool isVideo, CancellationToken cancellationToken)
+ {
+ var item = _libraryManager.ResolvePath(new FileInfo(path));
+
+ if (item != null)
+ {
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ public IEnumerable<ITaskTrigger> GetDefaultTriggers()
+ {
+ return new ITaskTrigger[]
+ {
+ new DailyTrigger { TimeOfDay = TimeSpan.FromHours(3) },
+ };
+ }
+
+ /// <summary>
+ /// Deletes the cache files from directory with a last write time less than a given date
+ /// </summary>
+ /// <param name="cancellationToken">The task cancellation token.</param>
+ /// <param name="directory">The directory.</param>
+ /// <param name="minDateModified">The min date modified.</param>
+ /// <param name="progress">The progress.</param>
+ private void DeleteCacheFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress)
+ {
+ var filesToDelete = new DirectoryInfo(directory).EnumerateFiles("*", SearchOption.AllDirectories)
+ .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
+
+ var index = 0;
+
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
+
+ progress.Report(100 * percent);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ DeleteFile(file.FullName);
+
+ index++;
+ }
+
+ progress.Report(100);
+ }
+
+ /// <summary>
+ /// Deletes the file.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ private void DeleteFile(string path)
+ {
+ try
+ {
+ File.Delete(path);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error deleting file {0}", ex, path);
+ }
+ }
+
+ public bool IsHidden
+ {
+ get
+ {
+ return !_manager.GetAllChannelFeatures()
+ .Any(i => i.CanDownloadAllMedia && _config.Configuration.ChannelOptions.DownloadingChannels.Contains(i.Id));
+ }
+ }
+
+ public bool IsEnabled
+ {
+ get
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
index b9b9b8327..b9e4e73b0 100644
--- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
+++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
@@ -54,6 +54,19 @@ namespace MediaBrowser.Server.Implementations.Channels
_factories = factories.ToArray();
}
+ public string ChannelDownloadPath
+ {
+ get
+ {
+ if (!string.IsNullOrWhiteSpace(_config.Configuration.ChannelOptions.DownloadPath))
+ {
+ return _config.Configuration.ChannelOptions.DownloadPath;
+ }
+
+ return Path.Combine(_config.ApplicationPaths.ProgramDataPath, "channels");
+ }
+ }
+
private IEnumerable<IChannel> GetAllChannels()
{
return _factories
@@ -156,7 +169,7 @@ namespace MediaBrowser.Server.Implementations.Channels
progress.Report(100);
}
- public Task<IEnumerable<ChannelMediaInfo>> GetChannelItemMediaSources(string id, CancellationToken cancellationToken)
+ public async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSources(string id, CancellationToken cancellationToken)
{
var item = (IChannelMediaItem)_libraryManager.GetItemById(id);
@@ -166,12 +179,149 @@ namespace MediaBrowser.Server.Implementations.Channels
var requiresCallback = channelPlugin as IRequiresMediaInfoCallback;
+ IEnumerable<ChannelMediaInfo> results;
+
if (requiresCallback != null)
{
- return requiresCallback.GetChannelItemMediaInfo(item.ExternalId, cancellationToken);
+ results = await requiresCallback.GetChannelItemMediaInfo(item.ExternalId, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ results = item.ChannelMediaSources;
}
- return Task.FromResult<IEnumerable<ChannelMediaInfo>>(item.ChannelMediaSources);
+ var sources = SortMediaInfoResults(results).Select(i => GetMediaSource(item, i))
+ .ToList();
+
+ var channelIdString = channel.Id.ToString("N");
+ var isVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+
+ var cachedVersionTasks = sources
+ .Select(i => GetCachedVersion(channelIdString, i, isVideo, cancellationToken));
+
+ var cachedVersions = await Task.WhenAll(cachedVersionTasks).ConfigureAwait(false);
+
+ sources.InsertRange(0, cachedVersions.Where(i => i != null));
+
+ return sources;
+ }
+
+ private MediaSourceInfo GetMediaSource(IChannelMediaItem item, ChannelMediaInfo info)
+ {
+ var id = info.Path.GetMD5().ToString("N");
+
+ var source = new MediaSourceInfo
+ {
+ MediaStreams = GetMediaStreams(info).ToList(),
+
+ Container = info.Container,
+ LocationType = info.IsRemote ? LocationType.Remote : LocationType.FileSystem,
+ Path = info.Path,
+ RequiredHttpHeaders = info.RequiredHttpHeaders,
+ RunTimeTicks = item.RunTimeTicks,
+ Name = id,
+ Id = id
+ };
+
+ return source;
+ }
+
+ private async Task<MediaSourceInfo> GetCachedVersion(string channelId,
+ MediaSourceInfo info,
+ bool isVideo,
+ CancellationToken cancellationToken)
+ {
+ var filename = info.Path.GetMD5().ToString("N");
+
+ var path = Path.Combine(ChannelDownloadPath, channelId, filename);
+
+ try
+ {
+ var file = Directory.EnumerateFiles(Path.GetDirectoryName(path), "*", SearchOption.TopDirectoryOnly)
+ .FirstOrDefault(i => (Path.GetFileName(i) ?? string.Empty).StartsWith(filename, StringComparison.OrdinalIgnoreCase));
+
+ if (!string.IsNullOrWhiteSpace(file))
+ {
+ var source = new MediaSourceInfo
+ {
+ Path = file,
+ LocationType = LocationType.FileSystem,
+ Name = "Cached " + info.Name,
+ Id = file.GetMD5().ToString("N")
+ };
+
+ if (isVideo)
+ {
+ source.VideoType = VideoType.VideoFile;
+ }
+
+ return source;
+ }
+ }
+ catch (DirectoryNotFoundException)
+ {
+ return null;
+ }
+
+ return null;
+ }
+
+ 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();
+
+ var width = _config.Configuration.ChannelOptions.PreferredStreamingWidth;
+
+ if (width.HasValue)
+ {
+ var val = width.Value;
+
+ return list
+ .OrderBy(i => i.Width.HasValue && i.Width.Value <= val)
+ .ThenBy(i => Math.Abs(i.Width ?? 0 - val))
+ .ThenByDescending(i => i.Width ?? 0)
+ .ThenBy(list.IndexOf);
+ }
+
+ return list
+ .OrderByDescending(i => i.Width ?? 0)
+ .ThenBy(list.IndexOf);
}
private async Task<Channel> GetChannel(IChannel channelInfo, CancellationToken cancellationToken)
@@ -237,26 +387,37 @@ namespace MediaBrowser.Server.Implementations.Channels
return (Channel)_libraryManager.GetItemById(new Guid(id));
}
+ public IEnumerable<ChannelFeatures> GetAllChannelFeatures()
+ {
+ return _channelEntities
+ .OrderBy(i => i.SortName)
+ .Select(i => GetChannelFeatures(i.Id.ToString("N")));
+ }
+
public ChannelFeatures GetChannelFeatures(string id)
{
var channel = GetChannel(id);
var channelProvider = GetChannelProvider(channel);
- return GetChannelFeaturesDto(channelProvider.GetChannelFeatures());
+ return GetChannelFeaturesDto(channel, channelProvider.GetChannelFeatures());
}
- public ChannelFeatures GetChannelFeaturesDto(InternalChannelFeatures features)
+ public ChannelFeatures GetChannelFeaturesDto(Channel channel, InternalChannelFeatures features)
{
return new ChannelFeatures
{
CanFilter = !features.MaxPageSize.HasValue,
+ CanGetAllMedia = features.CanGetAllMedia,
CanSearch = features.CanSearch,
ContentTypes = features.ContentTypes,
DefaultSortFields = features.DefaultSortFields,
MaxPageSize = features.MaxPageSize,
MediaTypes = features.MediaTypes,
- SupportsSortOrderToggle = features.SupportsSortOrderToggle
+ SupportsSortOrderToggle = features.SupportsSortOrderToggle,
+ Name = channel.Name,
+ Id = channel.Id.ToString("N"),
+ CanDownloadAllMedia = features.CanGetAllMedia
};
}
@@ -270,6 +431,85 @@ namespace MediaBrowser.Server.Implementations.Channels
return ("Channel " + name).GetMBId(typeof(Channel));
}
+ public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken)
+ {
+ var user = string.IsNullOrWhiteSpace(query.UserId)
+ ? null
+ : _userManager.GetUserById(new Guid(query.UserId));
+
+ var channels = _channels;
+
+ if (query.ChannelIds.Length > 0)
+ {
+ channels = channels
+ .Where(i => query.ChannelIds.Contains(GetInternalChannelId(i.Name).ToString("N")))
+ .ToArray();
+ }
+
+ var tasks = channels
+ .Where(i => i.GetChannelFeatures().CanGetAllMedia)
+ .Select(async i =>
+ {
+ try
+ {
+ var result = await i.GetAllMedia(new InternalAllChannelMediaQuery
+ {
+ User = user
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ return new Tuple<IChannel, ChannelItemResult>(i, result);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting all media from {0}", ex, i.Name);
+ return new Tuple<IChannel, ChannelItemResult>(i, new ChannelItemResult { });
+ }
+ });
+
+ var results = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ var totalCount = results.Length;
+
+ IEnumerable<Tuple<IChannel, ChannelItemInfo>> items = results
+ .SelectMany(i => i.Item2.Items.Select(m => new Tuple<IChannel, ChannelItemInfo>(i.Item1, m)))
+ .OrderBy(i => i.Item2.Name);
+
+ if (query.StartIndex.HasValue)
+ {
+ items = items.Skip(query.StartIndex.Value);
+ }
+ if (query.Limit.HasValue)
+ {
+ items = items.Take(query.Limit.Value);
+ }
+
+ // Avoid implicitly captured closure
+ var token = cancellationToken;
+ var itemTasks = items.Select(i =>
+ {
+ var channelProvider = i.Item1;
+ var channel = GetChannel(GetInternalChannelId(channelProvider.Name).ToString("N"));
+ return GetChannelItemEntity(i.Item2, channelProvider, channel, token);
+ });
+
+ var internalItems = await Task.WhenAll(itemTasks).ConfigureAwait(false);
+
+ // Get everything
+ var fields = Enum.GetNames(typeof(ItemFields))
+ .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
+ .ToList();
+
+ var returnItemArray = internalItems.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
+ .ToArray();
+
+ return new QueryResult<BaseItemDto>
+ {
+ TotalRecordCount = totalCount,
+ Items = returnItemArray
+ };
+ }
+
public async Task<QueryResult<BaseItemDto>> GetChannelItems(ChannelItemQuery query, CancellationToken cancellationToken)
{
var queryChannelId = query.ChannelId;
@@ -301,7 +541,7 @@ namespace MediaBrowser.Server.Implementations.Channels
ChannelItemSortField? sortField = null;
ChannelItemSortField parsedField;
- if (query.SortBy.Length == 1 &&
+ if (query.SortBy.Length == 1 &&
Enum.TryParse(query.SortBy[0], true, out parsedField))
{
sortField = parsedField;
@@ -309,11 +549,11 @@ namespace MediaBrowser.Server.Implementations.Channels
var sortDescending = query.SortOrder.HasValue && query.SortOrder.Value == SortOrder.Descending;
- var itemsResult = await GetChannelItems(channelProvider,
- user,
- query.FolderId,
- providerStartIndex,
- providerLimit,
+ var itemsResult = await GetChannelItems(channelProvider,
+ user,
+ query.FolderId,
+ providerStartIndex,
+ providerLimit,
sortField,
sortDescending,
cancellationToken)
diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs
index 88bf902db..520ffd417 100644
--- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs
+++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs
@@ -846,16 +846,7 @@ namespace MediaBrowser.Server.Implementations.Dto
if (fields.Contains(ItemFields.Path))
{
- var locationType = item.LocationType;
-
- if (locationType != LocationType.Remote && locationType != LocationType.Virtual)
- {
- dto.Path = GetMappedPath(item.Path);
- }
- else
- {
- dto.Path = item.Path;
- }
+ dto.Path = GetMappedPath(item);
}
dto.PremiereDate = item.PremiereDate;
@@ -1315,14 +1306,12 @@ namespace MediaBrowser.Server.Implementations.Dto
var locationType = item.LocationType;
- if (locationType != LocationType.FileSystem && locationType != LocationType.Offline)
- {
- return path;
- }
-
- foreach (var map in _config.Configuration.PathSubstitutions)
+ if (locationType == LocationType.FileSystem || locationType == LocationType.Offline)
{
- path = _fileSystem.SubstitutePath(path, map.From, map.To);
+ foreach (var map in _config.Configuration.PathSubstitutions)
+ {
+ path = _fileSystem.SubstitutePath(path, map.From, map.To);
+ }
}
return path;
@@ -1418,16 +1407,6 @@ namespace MediaBrowser.Server.Implementations.Dto
return string.Join("/", terms.ToArray());
}
- private string GetMappedPath(string path)
- {
- foreach (var map in _config.Configuration.PathSubstitutions)
- {
- path = _fileSystem.SubstitutePath(path, map.From, map.To);
- }
-
- return path;
- }
-
private void SetProductionLocations(BaseItem item, BaseItemDto dto)
{
var hasProductionLocations = item as IHasProductionLocations;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
index 0d0707386..df48e952f 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -297,6 +297,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var result = await service.GetRecordingStream(recording.Id, cancellationToken).ConfigureAwait(false);
+ Sanitize(result);
+
_logger.Debug("Live stream info: " + _json.SerializeToString(result));
if (!string.IsNullOrEmpty(result.Id))
@@ -332,6 +334,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var result = await service.GetChannelStream(channel.ExternalId, cancellationToken).ConfigureAwait(false);
+ Sanitize(result);
+
_logger.Debug("Live stream info: " + _json.SerializeToString(result));
if (!string.IsNullOrEmpty(result.Id))
@@ -353,6 +357,55 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
+ private void Sanitize(LiveStreamInfo info)
+ {
+ // Clean some bad data coming from providers
+
+ foreach (var stream in info.MediaStreams)
+ {
+ if (stream.BitDepth.HasValue && stream.BitDepth <= 0)
+ {
+ stream.BitDepth = null;
+ }
+ if (stream.BitRate.HasValue && stream.BitRate <= 0)
+ {
+ stream.BitRate = null;
+ }
+ if (stream.Channels.HasValue && stream.Channels <= 0)
+ {
+ stream.Channels = null;
+ }
+ if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0)
+ {
+ stream.AverageFrameRate = null;
+ }
+ if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0)
+ {
+ stream.RealFrameRate = null;
+ }
+ if (stream.Width.HasValue && stream.Width <= 0)
+ {
+ stream.Width = null;
+ }
+ if (stream.Height.HasValue && stream.Height <= 0)
+ {
+ stream.Height = null;
+ }
+ if (stream.SampleRate.HasValue && stream.SampleRate <= 0)
+ {
+ stream.SampleRate = null;
+ }
+ if (stream.Level.HasValue && stream.Level <= 0)
+ {
+ stream.Level = null;
+ }
+ if (stream.PacketLength.HasValue && stream.PacketLength <= 0)
+ {
+ stream.PacketLength = null;
+ }
+ }
+ }
+
private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken)
{
var path = Path.Combine(_config.ApplicationPaths.ItemsByNamePath, "tvchannels", _fileSystem.GetValidFilename(channelInfo.Name));
diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json
index 5c52a6095..cf827d26d 100644
--- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json
+++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json
@@ -136,5 +136,7 @@
"HeaderSelectServerCachePathHelp": "Browse or enter the path to use for server cache files. The folder must be writeable. The location of this folder will directly impact server performance and should ideally be placed on a solid state drive.",
"HeaderSelectTranscodingPathHelp": "Browse or enter the path to use for transcoding temporary files. The folder must be writeable.",
"HeaderSelectImagesByNamePathHelp": "Browse or enter the path to your items by name folder. The folder must be writeable.",
- "HeaderSelectMetadataPathHelp": "Browse or enter the path you'd like to store metadata within. The folder must be writeable."
+ "HeaderSelectMetadataPathHelp": "Browse or enter the path you'd like to store metadata within. The folder must be writeable.",
+ "HeaderSelectChannelDownloadPath": "Select Channel Download Path",
+ "HeaderSelectChannelDownloadPathHelp": "Browse or enter the path to use for storing channel cache files. The folder must be writeable."
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json
index a920b8e57..d7c923b58 100644
--- a/MediaBrowser.Server.Implementations/Localization/Server/server.json
+++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json
@@ -798,7 +798,13 @@
"ButtonDismiss": "Dismiss",
"MessageLearnHowToCustomize": "Learn how to customize this page to your own personal tastes. Click your user icon in the top right corner of the screen to view and update your preferences.",
"ButtonEditOtherUserPreferences": "Edit this user's personal preferences.",
- "ChannelStreamOptionBestAvailable": "Best available",
- "LabelChannelStreamOptionBestAvailable": "Preferred streaming quality:",
- "LabelChannelStreamOptionBestAvailableHelp": "Determines the selected quality when channel content is available in multiple resolutions."
+ "LabelChannelStreamQuality": "Preferred internet stream quality:",
+ "LabelChannelStreamQualityHelp": "In a low bandwidth environment, limiting quality can help ensure a smooth streaming experience.",
+ "OptionBestAvailableStreamQuality": "Best available",
+ "LabelEnableChannelContentDownloadingFor": "Enable channel content downloading for:",
+ "LabelEnableChannelContentDownloadingForHelp": "Some channels support downloading content prior to viewing. Enable this in low bandwidth enviornments to download channel content during off hours.",
+ "LabelChannelDownloadPath": "Channel content download path:",
+ "LabelChannelDownloadPathHelp": "Specify a custom download path if desired. Leave empty to download to an internal program data folder.",
+ "LabelChannelDownloadAge": "Delete content after: (days)",
+ "LabelChannelDownloadAgeHelp": "Downloaded content older than this will be deleted. It will remain playable via internet streaming."
} \ 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 efc7ebb6a..afe376217 100644
--- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
+++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
@@ -98,6 +98,7 @@
<Compile Include="..\SharedVersion.cs">
<Link>Properties\SharedVersion.cs</Link>
</Compile>
+ <Compile Include="Channels\ChannelDownloadScheduledTask.cs" />
<Compile Include="Channels\ChannelImageProvider.cs" />
<Compile Include="Channels\ChannelItemImageProvider.cs" />
<Compile Include="Channels\ChannelManager.cs" />
diff --git a/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs b/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs
index f1d4c3555..a3a6d3630 100644
--- a/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs
+++ b/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs
@@ -6,6 +6,7 @@ using MediaBrowser.Model.Session;
using MediaBrowser.Model.System;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
@@ -125,13 +126,16 @@ namespace MediaBrowser.Server.Implementations.Session
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
{
- return Task.FromResult(true);
- //return SendMessage(new WebSocketMessage<PlayRequest>
- //{
- // MessageType = "Play",
- // Data = command
+ var dict = new Dictionary<string, string>();
+
+ dict["ItemIds"] = string.Join(",", command.ItemIds);
+
+ if (command.StartPositionTicks.HasValue)
+ {
+ dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
+ }
- //}, cancellationToken);
+ return SendMessage(command.PlayCommand.ToString(), dict, cancellationToken);
}
public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
@@ -140,7 +144,12 @@ namespace MediaBrowser.Server.Implementations.Session
if (command.Command == PlaystateCommand.Seek)
{
+ if (!command.SeekPositionTicks.HasValue)
+ {
+ throw new ArgumentException("SeekPositionTicks cannot be null");
+ }
+ args["StartPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
}
return SendMessage(command.Command.ToString(), cancellationToken);