aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/LiveTv
diff options
context:
space:
mode:
authorLuke Pulverenti <luke.pulverenti@gmail.com>2016-02-14 16:35:09 -0500
committerLuke Pulverenti <luke.pulverenti@gmail.com>2016-02-14 16:35:09 -0500
commitdaa0b6cd0ecefd60611752802d062c15e6da85de (patch)
treef58a16e47afed9b61471e3871280faa9fc8bd951 /MediaBrowser.Server.Implementations/LiveTv
parent7d26b8995f313917829573a7cd96c37decc9158a (diff)
parentfd5f12e76227d96c52cdc31b67ef9543b485169b (diff)
Merge branch 'beta'
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv')
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs50
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs67
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs255
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs12
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs94
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs80
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp.cs54
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs142
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs45
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();
+ //}
+ }
+}