diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2016-02-14 16:35:09 -0500 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2016-02-14 16:35:09 -0500 |
| commit | daa0b6cd0ecefd60611752802d062c15e6da85de (patch) | |
| tree | f58a16e47afed9b61471e3871280faa9fc8bd951 /MediaBrowser.Server.Implementations/LiveTv | |
| parent | 7d26b8995f313917829573a7cd96c37decc9158a (diff) | |
| parent | fd5f12e76227d96c52cdc31b67ef9543b485169b (diff) | |
Merge branch 'beta'
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv')
9 files changed, 653 insertions, 146 deletions
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs new file mode 100644 index 000000000..9ac96165f --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV +{ + public class DirectRecorder : IRecorder + { + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + + public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem) + { + _logger = logger; + _httpClient = httpClient; + _fileSystem = fileSystem; + } + + public async Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken) + { + var httpRequestOptions = new HttpRequestOptions() + { + Url = mediaSource.Path + }; + + httpRequestOptions.BufferContent = false; + + using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false)) + { + _logger.Info("Opened recording stream from tuner provider"); + + using (var output = _fileSystem.GetFileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + onStarted(); + + _logger.Info("Copying recording stream to file stream"); + + await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index cd91684ce..9e4cb66a8 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -171,7 +171,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { epgData = GetEpgDataForChannel(timer.ChannelId); } - await UpdateTimersForSeriesTimer(epgData, timer, false).ConfigureAwait(false); + await UpdateTimersForSeriesTimer(epgData, timer, true).ConfigureAwait(false); } var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false); @@ -664,12 +664,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException("timer"); } + ProgramInfo info = null; + if (string.IsNullOrWhiteSpace(timer.ProgramId)) { - throw new InvalidOperationException("timer.ProgramId is null. Cannot record."); + _logger.Info("Timer {0} has null programId", timer.Id); + } + else + { + info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); } - var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); + if (info == null) + { + _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); + info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); + } if (info == null) { @@ -742,7 +752,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV try { - var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None); + var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); var mediaStreamInfo = result.Item1; var isResourceOpen = true; @@ -754,10 +764,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV var duration = recordingEndDate - DateTime.UtcNow; - HttpRequestOptions httpRequestOptions = new HttpRequestOptions() + var recorder = await GetRecorder().ConfigureAwait(false); + + if (recorder is EncodedRecorder) { - Url = mediaStreamInfo.Path - }; + recordPath = Path.ChangeExtension(recordPath, ".mp4"); + } recording.Path = recordPath; recording.Status = RecordingStatus.InProgress; @@ -766,21 +778,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture)); - httpRequestOptions.BufferContent = false; var durationToken = new CancellationTokenSource(duration); var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - httpRequestOptions.CancellationToken = linkedToken; + _logger.Info("Writing file to path: " + recordPath); - using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET")) + _logger.Info("Opening recording stream from tuner provider"); + + Action onStarted = () => { - using (var output = _fileSystem.GetFileStream(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - result.Item2.Release(); - isResourceOpen = false; + result.Item2.Release(); + isResourceOpen = false; + }; - await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken); - } - } + await recorder.Record(mediaStreamInfo, recordPath, onStarted, linkedToken).ConfigureAwait(false); recording.Status = RecordingStatus.Completed; _logger.Info("Recording completed"); @@ -831,6 +841,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } + private async Task<IRecorder> GetRecorder() + { + if (GetConfiguration().EnableRecordingEncoding) + { + var regInfo = await _security.GetRegistrationStatus("embytvrecordingconversion").ConfigureAwait(false); + + if (regInfo.IsValid) + { + return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); + } + } + + return new DirectRecorder(_logger, _httpClient, _fileSystem); + } + private async void OnSuccessfulRecording(RecordingInfo recording) { if (GetConfiguration().EnableAutoOrganize) @@ -862,6 +887,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase)); } + private ProgramInfo GetProgramInfoFromCache(string channelId, DateTime startDateUtc) + { + var epgData = GetEpgDataForChannel(channelId); + var startDateTicks = startDateUtc.Ticks; + // Find the first program that starts within 3 minutes + return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks); + } + private string RecordingPath { get diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs new file mode 100644 index 000000000..b4ff79567 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV +{ + public class EncodedRecorder : IRecorder + { + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IMediaEncoder _mediaEncoder; + private readonly IApplicationPaths _appPaths; + private bool _hasExited; + private Stream _logFileStream; + private string _targetPath; + private Process _process; + private readonly IJsonSerializer _json; + + public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IApplicationPaths appPaths, IJsonSerializer json) + { + _logger = logger; + _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; + _appPaths = appPaths; + _json = json; + } + + public async Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken) + { + _targetPath = targetFile; + _fileSystem.CreateDirectory(Path.GetDirectoryName(targetFile)); + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + + // Must consume both stdout and stderr or deadlocks may occur + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + + FileName = _mediaEncoder.EncoderPath, + Arguments = GetCommandLineArgs(mediaSource, targetFile), + + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + + EnableRaisingEvents = true + }; + + _process = process; + + var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; + _logger.Info(commandLineLogMessage); + + var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt"); + _fileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath)); + + // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. + _logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); + + var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); + await _logFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false); + + process.Exited += (sender, args) => OnFfMpegProcessExited(process); + + process.Start(); + + cancellationToken.Register(Stop); + + // MUST read both stdout and stderr asynchronously or a deadlock may occurr + process.BeginOutputReadLine(); + + // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback + StartStreamingLog(process.StandardError.BaseStream, _logFileStream); + + // Wait for the file to exist before proceeeding + while (!_hasExited) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + } + + private string GetCommandLineArgs(MediaSourceInfo mediaSource, string targetFile) + { + string videoArgs; + if (EncodeVideo(mediaSource)) + { + var maxBitrate = 25000000; + videoArgs = string.Format( + "-codec:v:0 libx264 -force_key_frames expr:gte(t,n_forced*5) {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync vfr -profile:v high -level 41", + GetOutputSizeParam(), + maxBitrate.ToString(CultureInfo.InvariantCulture)); + } + else + { + videoArgs = "-codec:v:0 copy"; + } + + var commandLineArgs = "-fflags +genpts -i \"{0}\" -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\""; + + if (mediaSource.ReadAtNativeFramerate) + { + commandLineArgs = "-re " + commandLineArgs; + } + + commandLineArgs = string.Format(commandLineArgs, mediaSource.Path, targetFile, videoArgs, GetAudioArgs(mediaSource)); + + return commandLineArgs; + } + + private string GetAudioArgs(MediaSourceInfo mediaSource) + { + var copyAudio = new[] { "aac", "mp3" }; + var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); + if (mediaStreams.Any(i => i.Type == MediaStreamType.Audio && copyAudio.Contains(i.Codec, StringComparer.OrdinalIgnoreCase))) + { + return "-codec:a:0 copy"; + } + + var audioChannels = 2; + var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + if (audioStream != null) + { + audioChannels = audioStream.Channels ?? audioChannels; + } + return "-codec:a:0 aac -strict experimental -ab 320000 -ac " + audioChannels.ToString(CultureInfo.InvariantCulture); + } + + private bool EncodeVideo(MediaSourceInfo mediaSource) + { + var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); + return !mediaStreams.Any(i => i.Type == MediaStreamType.Video && string.Equals(i.Codec, "h264", StringComparison.OrdinalIgnoreCase) && !i.IsInterlaced); + } + + protected string GetOutputSizeParam() + { + var filters = new List<string>(); + + filters.Add("yadif=0:-1:0"); + + var output = string.Empty; + + if (filters.Count > 0) + { + output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray())); + } + + return output; + } + + private void Stop() + { + if (!_hasExited) + { + try + { + _logger.Info("Killing ffmpeg recording process for {0}", _targetPath); + + //process.Kill(); + _process.StandardInput.WriteLine("q"); + + // Need to wait because killing is asynchronous + _process.WaitForExit(5000); + } + catch (Exception ex) + { + _logger.ErrorException("Error killing transcoding job for {0}", ex, _targetPath); + } + } + } + + /// <summary> + /// Processes the exited. + /// </summary> + /// <param name="process">The process.</param> + private void OnFfMpegProcessExited(Process process) + { + _hasExited = true; + + _logger.Debug("Disposing stream resources"); + DisposeLogStream(); + + try + { + _logger.Info("FFMpeg exited with code {0}", process.ExitCode); + } + catch + { + _logger.Error("FFMpeg exited with an error."); + } + } + + private void DisposeLogStream() + { + if (_logFileStream != null) + { + try + { + _logFileStream.Dispose(); + } + catch (Exception ex) + { + _logger.ErrorException("Error disposing log stream", ex); + } + + _logFileStream = null; + } + } + + private async void StartStreamingLog(Stream source, Stream target) + { + try + { + using (var reader = new StreamReader(source)) + { + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); + + await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + await target.FlushAsync().ConfigureAwait(false); + } + } + } + catch (ObjectDisposedException) + { + // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux + } + catch (Exception ex) + { + _logger.ErrorException("Error reading ffmpeg log", ex); + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs new file mode 100644 index 000000000..12e73c1f3 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV +{ + public interface IRecorder + { + Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 8bf1d27b8..14bfcba27 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -300,6 +300,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task<ILiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken) { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + var result = await GetInternalRecordings(new RecordingQuery { Id = id @@ -560,6 +565,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv } item.ExternalId = channelInfo.Id; + if (!item.ParentId.Equals(parentFolderId)) + { + isNew = true; + } + item.ParentId = parentFolderId; + item.ChannelType = channelInfo.ChannelType; item.ServiceName = serviceName; item.Number = channelInfo.Number; @@ -622,6 +633,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } + if (!item.ParentId.Equals(channel.Id)) + { + forceUpdate = true; + } + item.ParentId = channel.Id; + //item.ChannelType = channelType; if (!string.Equals(item.ServiceName, serviceName, StringComparison.Ordinal)) { @@ -774,6 +791,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv } recording.IsSeries = info.IsSeries; + if (!item.ParentId.Equals(parentFolderId)) + { + dataChanged = true; + } + item.ParentId = parentFolderId; + if (!item.HasImage(ImageType.Primary)) { if (!string.IsNullOrWhiteSpace(info.ImagePath)) @@ -851,7 +874,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); - var internalQuery = new InternalItemsQuery + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, MinEndDate = query.MinEndDate, @@ -869,16 +892,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv SortOrder = query.SortOrder ?? SortOrder.Ascending }; - if (user != null) - { - internalQuery.MaxParentalRating = user.Policy.MaxParentalRating; - - if (user.Policy.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram)) - { - internalQuery.HasParentalRating = true; - } - } - if (query.HasAired.HasValue) { if (query.HasAired.Value) @@ -913,7 +926,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = _userManager.GetUserById(query.UserId); - var internalQuery = new InternalItemsQuery + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IsAiring = query.IsAiring, @@ -922,16 +935,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv IsKids = query.IsKids }; - if (user != null) - { - internalQuery.MaxParentalRating = user.Policy.MaxParentalRating; - - if (user.Policy.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram)) - { - internalQuery.HasParentalRating = true; - } - } - if (query.HasAired.HasValue) { if (query.HasAired.Value) @@ -1399,7 +1402,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv await RefreshRecordings(cancellationToken).ConfigureAwait(false); - var internalQuery = new InternalItemsQuery + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvVideoRecording).Name, typeof(LiveTvAudioRecording).Name } }; @@ -1409,10 +1412,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv internalQuery.ChannelIds = new[] { query.ChannelId }; } - var queryResult = _libraryManager.GetItems(internalQuery); - IEnumerable<ILiveTvRecording> recordings = queryResult.Items.Cast<ILiveTvRecording>(); + var queryResult = _libraryManager.GetItems(internalQuery, new string[] { }); + IEnumerable<ILiveTvRecording> recordings = queryResult.Cast<ILiveTvRecording>(); - if (!string.IsNullOrEmpty(query.Id)) + if (!string.IsNullOrWhiteSpace(query.Id)) { var guid = new Guid(query.Id); @@ -1420,7 +1423,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv .Where(i => i.Id == guid); } - if (!string.IsNullOrEmpty(query.GroupId)) + if (!string.IsNullOrWhiteSpace(query.GroupId)) { var guid = new Guid(query.GroupId); @@ -1469,7 +1472,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } - public void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, bool addChannelInfo, User user = null) + public void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, List<ItemFields> fields, User user = null) { var program = (LiveTvProgram)item; @@ -1509,13 +1512,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.IsPremiere = program.IsPremiere; } - if (addChannelInfo) + if (fields.Contains(ItemFields.ChannelInfo)) { var channel = GetInternalChannel(program.ChannelId); if (channel != null) { dto.ChannelName = channel.Name; + dto.MediaType = channel.MediaType; if (channel.HasImage(ImageType.Primary)) { @@ -1523,6 +1527,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } } + + if (fields.Contains(ItemFields.ServiceName)) + { + var service = GetService(program); + if (service != null) + { + dto.ServiceName = service.Name; + } + } } public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, User user = null) @@ -1593,18 +1606,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv var internalResult = await GetInternalRecordings(query, cancellationToken).ConfigureAwait(false); - var returnArray = internalResult.Items - .Select(i => _dtoService.GetBaseItemDto(i, options, user)) + var tuples = internalResult.Items + .Select(i => new Tuple<BaseItem, BaseItemDto>(i, _dtoService.GetBaseItemDto(i, options, user))) .ToArray(); if (user != null) { - _dtoService.FillSyncInfo(returnArray, new DtoOptions(), user); + _dtoService.FillSyncInfo(tuples, new DtoOptions(), user); } return new QueryResult<BaseItemDto> { - Items = returnArray, + Items = tuples.Select(i => i.Item2).ToArray(), TotalRecordCount = internalResult.TotalRecordCount }; } @@ -1674,6 +1687,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv throw new ResourceNotFoundException(string.Format("Recording with Id {0} not found", recordingId)); } + await DeleteRecording(recording).ConfigureAwait(false); + } + + public async Task DeleteRecording(ILiveTvRecording recording) + { var service = GetService(recording.ServiceName); try @@ -1685,6 +1703,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv } + _lastRecordingRefreshTime = DateTime.MinValue; + // This is the responsibility of the live tv service await _libraryManager.DeleteItem((BaseItem)recording, new DeleteOptions { @@ -1812,7 +1832,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var now = DateTime.UtcNow; - var programs = _libraryManager.GetItems(new InternalItemsQuery + var programs = _libraryManager.GetItems(new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, ChannelIds = new[] { id }, @@ -1821,7 +1841,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv Limit = 1, SortBy = new[] { "StartDate" } - }).Items.Cast<LiveTvProgram>(); + }, new string[] { }).Cast<LiveTvProgram>(); var currentProgram = programs.FirstOrDefault(); @@ -1836,7 +1856,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var now = DateTime.UtcNow; - var programs = _libraryManager.GetItems(new InternalItemsQuery + var programs = _libraryManager.GetItems(new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, ChannelIds = new[] { channel.Id.ToString("N") }, @@ -1845,7 +1865,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv Limit = 1, SortBy = new[] { "StartDate" } - }).Items.Cast<LiveTvProgram>(); + }, new string[] { }).Cast<LiveTvProgram>(); var currentProgram = programs.FirstOrDefault(); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index ddbbb030d..f87d4f43f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -46,53 +46,59 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) { - var url = info.Url; - var urlHash = url.GetMD5().ToString("N"); + var urlHash = info.Url.GetMD5().ToString("N"); - string line; // Read the file and display it line by line. - using (var file = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) + using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) { - var channels = new List<M3UChannel>(); + return GetChannels(reader, urlHash); + } + } + + private List<M3UChannel> GetChannels(StreamReader reader, string urlHash) + { + var channels = new List<M3UChannel>(); - string channnelName = null; - string channelNumber = null; + string channnelName = null; + string channelNumber = null; + string line; - while ((line = file.ReadLine()) != null) + while ((line = reader.ReadLine()) != null) + { + line = line.Trim(); + if (string.IsNullOrWhiteSpace(line)) { - line = line.Trim(); - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } + continue; + } - if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase)) - { - var parts = line.Split(new[] { ':' }, 2).Last().Split(new[] { ',' }, 2); - channelNumber = parts[0]; - channnelName = parts[1]; - } - else if (!string.IsNullOrWhiteSpace(channelNumber)) + if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase)) + { + line = line.Substring(8); + Logger.Info("Found m3u channel: {0}", line); + var parts = line.Split(new[] { ',' }, 2); + channelNumber = parts[0]; + channnelName = parts[1]; + } + else if (!string.IsNullOrWhiteSpace(channelNumber)) + { + channels.Add(new M3UChannel { - channels.Add(new M3UChannel - { - Name = channnelName, - Number = channelNumber, - Id = ChannelIdPrefix + urlHash + channelNumber, - Path = line - }); - - channelNumber = null; - channnelName = null; - } + Name = channnelName, + Number = channelNumber, + Id = ChannelIdPrefix + urlHash + line.GetMD5().ToString("N"), + Path = line + }); + + channelNumber = null; + channnelName = null; } - return channels; } + return channels; } public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) @@ -159,8 +165,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts return null; } - //channelId = channelId.Substring(prefix.Length); - var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false); var m3uchannels = channels.Cast<M3UChannel>(); var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase)); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp.cs deleted file mode 100644 index ecd2864c5..000000000 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts -{ - public class SatIp : BaseTunerHost - { - public SatIp(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) - : base(config, logger, jsonSerializer, mediaEncoder) - { - } - - protected override Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public override string Type - { - get { return "SatIp"; } - } - - protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected override Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected override bool IsValidChannelId(string channelId) - { - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs new file mode 100644 index 000000000..95c04d61f --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp +{ + public class SatIpDiscovery : IServerEntryPoint + { + private readonly IDeviceDiscovery _deviceDiscovery; + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly ILiveTvManager _liveTvManager; + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly IHttpClient _httpClient; + + public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient) + { + _deviceDiscovery = deviceDiscovery; + _config = config; + _logger = logger; + _liveTvManager = liveTvManager; + _httpClient = httpClient; + } + + public void Run() + { + _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; + } + + void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + { + //string server = null; + //if (e.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) + //{ + // string location; + // if (e.Headers.TryGetValue("Location", out location)) + // { + // //_logger.Debug("HdHomerun found at {0}", location); + + // // Just get the beginning of the url + // Uri uri; + // if (Uri.TryCreate(location, UriKind.Absolute, out uri)) + // { + // var apiUrl = location.Replace(uri.LocalPath, String.Empty, StringComparison.OrdinalIgnoreCase) + // .TrimEnd('/'); + + // //_logger.Debug("HdHomerun api url: {0}", apiUrl); + // AddDevice(apiUrl); + // } + // } + //} + } + + private async void AddDevice(string url) + { + await _semaphore.WaitAsync().ConfigureAwait(false); + + try + { + var options = GetConfiguration(); + + if (options.TunerHosts.Any(i => + string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) && + UriEquals(i.Url, url))) + { + return; + } + + // Strip off the port + url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/'); + + await TestUrl(url).ConfigureAwait(false); + + await _liveTvManager.SaveTunerHost(new TunerHostInfo + { + Type = SatIpHost.DeviceType, + Url = url + + }).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error saving device", ex); + } + finally + { + _semaphore.Release(); + } + } + + private async Task TestUrl(string url) + { + // Test it by pulling down the lineup + using (await _httpClient.Get(new HttpRequestOptions + { + Url = string.Format("{0}/lineup.json", url), + CancellationToken = CancellationToken.None + })) + { + } + } + + private bool UriEquals(string savedUri, string location) + { + return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase); + } + + private string NormalizeUrl(string url) + { + if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + url = "http://" + url; + } + + url = url.TrimEnd('/'); + + // Strip off the port + return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped); + } + + private LiveTvOptions GetConfiguration() + { + return _config.GetConfiguration<LiveTvOptions>("livetv"); + } + + public void Dispose() + { + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs new file mode 100644 index 000000000..205cdf74e --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs @@ -0,0 +1,45 @@ +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp +{ + public class SatIpHost /*: BaseTunerHost*/ + { + //public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) + // : base(config, logger, jsonSerializer, mediaEncoder) + //{ + //} + + //protected override Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + public static string DeviceType + { + get { return "satip"; } + } + + //public override string Type + //{ + // get { return DeviceType; } + //} + + //protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + //protected override Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + //protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + //protected override bool IsValidChannelId(string channelId) + //{ + // throw new NotImplementedException(); + //} + } +} |
