aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/LiveTv
diff options
context:
space:
mode:
authorLuke <luke.pulverenti@gmail.com>2016-03-02 16:06:25 -0500
committerLuke <luke.pulverenti@gmail.com>2016-03-02 16:06:25 -0500
commit8fc7d7ba026ed871524055738dc33ddcac5e674d (patch)
treeb29a3df950c4c4d560701dc4bd1a9a2529dc53ea /MediaBrowser.Server.Implementations/LiveTv
parent9638b242a4c8f614ed4ffa256422cd0ba3a029e2 (diff)
parent81e96ed4f678b4de114e9d03844141ae65b5856b (diff)
Merge pull request #1514 from MediaBrowser/beta
Beta
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv')
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs7
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs127
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs16
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs11
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs13
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs10
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs5
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs3
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs144
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs2
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs3
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs74
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs81
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs132
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs199
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs186
16 files changed, 723 insertions, 290 deletions
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 9ac96165f..d33b2c51d 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -23,7 +23,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_fileSystem = fileSystem;
}
- public async Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken)
+ public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
var httpRequestOptions = new HttpRequestOptions()
{
@@ -42,7 +42,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_logger.Info("Copying recording stream to file stream");
- await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ var durationToken = new CancellationTokenSource(duration);
+ var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+
+ await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken).ConfigureAwait(false);
}
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 9e4cb66a8..3ab08274c 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -87,7 +87,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_timerProvider.RestartTimers();
SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
-
}
void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
@@ -104,8 +103,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
- private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeRecordings =
- new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
+ new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
public string Name
{
@@ -210,9 +209,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- if (list.Count > 0)
+ foreach (var provider in GetListingProviders())
{
- foreach (var provider in GetListingProviders())
+ var enabledChannels = list
+ .Where(i => IsListingProviderEnabledForTuner(provider.Item2, i.TunerHostId))
+ .ToList();
+
+ if (enabledChannels.Count > 0)
{
try
{
@@ -228,6 +231,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
}
+
_channelCache = list;
return list;
}
@@ -264,11 +268,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
_timerProvider.Delete(remove);
}
- CancellationTokenSource cancellationTokenSource;
+ ActiveRecordingInfo activeRecordingInfo;
- if (_activeRecordings.TryGetValue(timerId, out cancellationTokenSource))
+ if (_activeRecordings.TryGetValue(timerId, out activeRecordingInfo))
{
- cancellationTokenSource.Cancel();
+ activeRecordingInfo.CancellationTokenSource.Cancel();
}
}
@@ -296,17 +300,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- try
- {
- _fileSystem.DeleteFile(remove.Path);
- }
- catch (DirectoryNotFoundException)
+ if (!string.IsNullOrWhiteSpace(remove.Path))
{
+ try
+ {
+ _fileSystem.DeleteFile(remove.Path);
+ }
+ catch (DirectoryNotFoundException)
+ {
- }
- catch (FileNotFoundException)
- {
+ }
+ catch (FileNotFoundException)
+ {
+ }
}
_recordingProvider.Delete(remove);
}
@@ -489,6 +496,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
+ private bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId)
+ {
+ return info.EnableAllTuners || info.EnabledTuners.Contains(tunerHostId ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ }
+
private async Task<IEnumerable<ProgramInfo>> GetProgramsAsyncInternal(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
{
var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
@@ -496,6 +508,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
foreach (var provider in GetListingProviders())
{
+ if (!IsListingProviderEnabledForTuner(provider.Item2, channel.TunerHostId))
+ {
+ continue;
+ }
+
var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, channel.Name, startDateUtc, endDateUtc, cancellationToken)
.ConfigureAwait(false);
@@ -636,11 +653,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return;
}
- var cancellationTokenSource = new CancellationTokenSource();
+ var activeRecordingInfo = new ActiveRecordingInfo
+ {
+ CancellationTokenSource = new CancellationTokenSource(),
+ TimerId = timer.Id
+ };
- if (_activeRecordings.TryAdd(timer.Id, cancellationTokenSource))
+ if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo))
{
- await RecordStream(timer, recordingEndDate, cancellationTokenSource.Token).ConfigureAwait(false);
+ await RecordStream(timer, recordingEndDate, activeRecordingInfo, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
}
else
{
@@ -657,7 +678,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, CancellationToken cancellationToken)
+ private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
{
if (timer == null)
{
@@ -712,7 +733,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
recordPath = Path.Combine(recordPath, recordingFileName);
- _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath));
var recordingId = info.Id.GetMD5().ToString("N");
var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase));
@@ -760,7 +780,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
try
{
// HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
- await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
+ //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
var duration = recordingEndDate - DateTime.UtcNow;
@@ -770,6 +790,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.ChangeExtension(recordPath, ".mp4");
}
+ recordPath = EnsureFileUnique(recordPath, timer.Id);
+ _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath));
+ activeRecordingInfo.Path = recordPath;
+
+ _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
recording.Path = recordPath;
recording.Status = RecordingStatus.InProgress;
@@ -778,9 +803,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
- var durationToken = new CancellationTokenSource(duration);
- var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
-
_logger.Info("Writing file to path: " + recordPath);
_logger.Info("Opening recording stream from tuner provider");
@@ -790,10 +812,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
isResourceOpen = false;
};
- await recorder.Record(mediaStreamInfo, recordPath, onStarted, linkedToken).ConfigureAwait(false);
+ await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
recording.Status = RecordingStatus.Completed;
- _logger.Info("Recording completed");
+ _logger.Info("Recording completed: {0}", recordPath);
}
finally
{
@@ -801,21 +823,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
result.Item2.Release();
}
+
+ _libraryMonitor.ReportFileSystemChangeComplete(recordPath, false);
}
}
catch (OperationCanceledException)
{
- _logger.Info("Recording stopped");
+ _logger.Info("Recording stopped: {0}", recordPath);
recording.Status = RecordingStatus.Completed;
}
catch (Exception ex)
{
- _logger.ErrorException("Error recording", ex);
+ _logger.ErrorException("Error recording to {0}", ex, recordPath);
recording.Status = RecordingStatus.Error;
}
finally
{
- CancellationTokenSource removed;
+ ActiveRecordingInfo removed;
_activeRecordings.TryRemove(timer.Id, out removed);
}
@@ -841,6 +865,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
+ private string EnsureFileUnique(string path, string timerId)
+ {
+ var originalPath = path;
+ var index = 1;
+
+ while (FileExists(path, timerId))
+ {
+ var parent = Path.GetDirectoryName(originalPath);
+ var name = Path.GetFileNameWithoutExtension(originalPath);
+ name += "-" + index.ToString(CultureInfo.InvariantCulture);
+
+ path = Path.ChangeExtension(Path.Combine(parent, name), Path.GetExtension(originalPath));
+ index++;
+ }
+
+ return path;
+ }
+
+ private bool FileExists(string path, string timerId)
+ {
+ if (_fileSystem.FileExists(path))
+ {
+ return true;
+ }
+
+ var hasRecordingAtPath = _activeRecordings.Values.ToList().Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.TimerId, timerId, StringComparison.OrdinalIgnoreCase));
+
+ if (hasRecordingAtPath)
+ {
+ return true;
+ }
+ return false;
+ }
+
private async Task<IRecorder> GetRecorder()
{
if (GetConfiguration().EnableRecordingEncoding)
@@ -1024,7 +1082,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
foreach (var pair in _activeRecordings.ToList())
{
- pair.Value.Cancel();
+ pair.Value.CancellationTokenSource.Cancel();
}
}
@@ -1041,5 +1099,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
IsRegistered = true
});
}
+
+ class ActiveRecordingInfo
+ {
+ public string Path { get; set; }
+ public string TimerId { get; set; }
+ public CancellationTokenSource CancellationTokenSource { get; set; }
+ }
}
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 5f4d32732..62c9cd171 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -38,7 +38,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_json = json;
}
- public async Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken)
+ public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;
_fileSystem.CreateDirectory(Path.GetDirectoryName(targetFile));
@@ -56,7 +56,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
RedirectStandardInput = true,
FileName = _mediaEncoder.EncoderPath,
- Arguments = GetCommandLineArgs(mediaSource, targetFile),
+ Arguments = GetCommandLineArgs(mediaSource, targetFile, duration),
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
@@ -88,11 +88,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
process.BeginOutputReadLine();
+ onStarted();
+
// 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);
- onStarted();
-
// Wait for the file to exist before proceeeding
while (!_hasExited)
{
@@ -100,7 +100,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- private string GetCommandLineArgs(MediaSourceInfo mediaSource, string targetFile)
+ private string GetCommandLineArgs(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration)
{
string videoArgs;
if (EncodeVideo(mediaSource))
@@ -116,14 +116,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
videoArgs = "-codec:v:0 copy";
}
- var commandLineArgs = "-fflags +genpts -i \"{0}\" -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
+ var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -i \"{0}\" -t {4} -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));
+ commandLineArgs = string.Format(commandLineArgs, mediaSource.Path, targetFile, videoArgs, GetAudioArgs(mediaSource), _mediaEncoder.GetTimeParameter(duration.Ticks));
return commandLineArgs;
}
@@ -143,7 +143,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
audioChannels = audioStream.Channels ?? audioChannels;
}
- return "-codec:a:0 aac -strict experimental -ab 320000 -ac " + audioChannels.ToString(CultureInfo.InvariantCulture);
+ return "-codec:a:0 aac -strict experimental -ab 320000";
}
private bool EncodeVideo(MediaSourceInfo mediaSource)
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
index 12e73c1f3..268a4f751 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
@@ -7,6 +7,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
public interface IRecorder
{
- Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken);
+ /// <summary>
+ /// Records the specified media source.
+ /// </summary>
+ /// <param name="mediaSource">The media source.</param>
+ /// <param name="targetFile">The target file.</param>
+ /// <param name="duration">The duration.</param>
+ /// <param name="onStarted">The on started.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index b29a7562c..68b3f1f71 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -13,7 +13,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
where T : class
{
private readonly object _fileDataLock = new object();
- private volatile List<T> _items;
+ private List<T> _items;
private readonly IJsonSerializer _jsonSerializer;
protected readonly ILogger Logger;
private readonly string _dataPath;
@@ -31,17 +31,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public IReadOnlyList<T> GetAll()
{
- if (_items == null)
+ lock (_fileDataLock)
{
- lock (_fileDataLock)
+ if (_items == null)
{
- if (_items == null)
- {
- _items = GetItemsFromFile(_dataPath);
- }
+ _items = GetItemsFromFile(_dataPath);
}
+ return _items;
}
- return _items;
}
private List<T> GetItemsFromFile(string path)
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
index a5c869d45..37e10d925 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -56,13 +56,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
name += " " + info.OriginalAirDate.Value.ToString("yyyy-MM-dd");
}
- if (addHyphen)
- {
- name += " -";
- }
-
if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
{
+ if (addHyphen)
+ {
+ name += " -";
+ }
+
name += " " + info.EpisodeTitle;
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 87d7ff3eb..449943229 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -951,6 +951,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
public string stationID { get; set; }
public List<Program> programs { get; set; }
public MetadataSchedule metadata { get; set; }
+
+ public Day()
+ {
+ programs = new List<Program>();
+ }
}
//
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 04f99cdce..81ad6a387 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -178,7 +178,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
SourceType = info.SourceType,
Status = info.Status,
ChannelName = channelName,
- Url = info.Url
+ Url = info.Url,
+ CanReset = info.CanReset
};
if (!string.IsNullOrEmpty(info.ChannelId))
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
index 14bfcba27..abb2710e7 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -350,7 +350,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
foreach (var source in list)
{
- Normalize(source, item.ChannelType == ChannelType.TV);
+ Normalize(source, service, item.ChannelType == ChannelType.TV);
}
return list;
@@ -379,12 +379,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
MediaSourceInfo info;
bool isVideo;
+ ILiveTvService service;
if (isChannel)
{
var channel = GetInternalChannel(id);
isVideo = channel.ChannelType == ChannelType.TV;
- var service = GetService(channel);
+ service = GetService(channel);
_logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
info.RequiresClosing = true;
@@ -400,7 +401,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase);
- var service = GetService(recording);
+ service = GetService(recording);
_logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.ExternalId);
info = await service.GetRecordingStream(recording.ExternalId, null, cancellationToken).ConfigureAwait(false);
@@ -415,7 +416,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
_logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
- Normalize(info, isVideo);
+ Normalize(info, service, isVideo);
var data = new LiveStreamData
{
@@ -440,7 +441,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
- private void Normalize(MediaSourceInfo mediaSource, bool isVideo)
+ private void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)
{
if (mediaSource.MediaStreams.Count == 0)
{
@@ -537,6 +538,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
mediaSource.Bitrate = total;
}
}
+
+ if (!(service is EmbyTV.EmbyTV))
+ {
+ // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says
+ mediaSource.SupportsDirectStream = true;
+ }
}
private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, Guid parentFolderId, CancellationToken cancellationToken)
@@ -801,11 +808,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.ImagePath))
{
- item.SetImagePath(ImageType.Primary, info.ImagePath);
+ item.SetImage(new ItemImageInfo
+ {
+ Path = info.ImagePath,
+ Type = ImageType.Primary,
+ IsPlaceholder = true
+ }, 0);
}
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
{
- item.SetImagePath(ImageType.Primary, info.ImageUrl);
+ item.SetImage(new ItemImageInfo
+ {
+ Path = info.ImageUrl,
+ Type = ImageType.Primary,
+ IsPlaceholder = true
+ }, 0);
}
}
@@ -1472,70 +1489,79 @@ namespace MediaBrowser.Server.Implementations.LiveTv
};
}
- public void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, List<ItemFields> fields, User user = null)
+ public async Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, List<ItemFields> fields, User user = null)
{
- var program = (LiveTvProgram)item;
-
- dto.StartDate = program.StartDate;
- dto.EpisodeTitle = program.EpisodeTitle;
+ var recordingTuples = new List<Tuple<BaseItemDto, string, string>>();
- if (program.IsRepeat)
- {
- dto.IsRepeat = program.IsRepeat;
- }
- if (program.IsMovie)
- {
- dto.IsMovie = program.IsMovie;
- }
- if (program.IsSeries)
- {
- dto.IsSeries = program.IsSeries;
- }
- if (program.IsSports)
- {
- dto.IsSports = program.IsSports;
- }
- if (program.IsLive)
- {
- dto.IsLive = program.IsLive;
- }
- if (program.IsNews)
- {
- dto.IsNews = program.IsNews;
- }
- if (program.IsKids)
- {
- dto.IsKids = program.IsKids;
- }
- if (program.IsPremiere)
+ foreach (var tuple in tuples)
{
- dto.IsPremiere = program.IsPremiere;
- }
+ var program = (LiveTvProgram)tuple.Item1;
+ var dto = tuple.Item2;
- if (fields.Contains(ItemFields.ChannelInfo))
- {
- var channel = GetInternalChannel(program.ChannelId);
+ dto.StartDate = program.StartDate;
+ dto.EpisodeTitle = program.EpisodeTitle;
+
+ if (program.IsRepeat)
+ {
+ dto.IsRepeat = program.IsRepeat;
+ }
+ if (program.IsMovie)
+ {
+ dto.IsMovie = program.IsMovie;
+ }
+ if (program.IsSeries)
+ {
+ dto.IsSeries = program.IsSeries;
+ }
+ if (program.IsSports)
+ {
+ dto.IsSports = program.IsSports;
+ }
+ if (program.IsLive)
+ {
+ dto.IsLive = program.IsLive;
+ }
+ if (program.IsNews)
+ {
+ dto.IsNews = program.IsNews;
+ }
+ if (program.IsKids)
+ {
+ dto.IsKids = program.IsKids;
+ }
+ if (program.IsPremiere)
+ {
+ dto.IsPremiere = program.IsPremiere;
+ }
- if (channel != null)
+ if (fields.Contains(ItemFields.ChannelInfo))
{
- dto.ChannelName = channel.Name;
- dto.MediaType = channel.MediaType;
+ var channel = GetInternalChannel(program.ChannelId);
- if (channel.HasImage(ImageType.Primary))
+ if (channel != null)
{
- dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel);
+ dto.ChannelName = channel.Name;
+ dto.MediaType = channel.MediaType;
+
+ if (channel.HasImage(ImageType.Primary))
+ {
+ dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel);
+ }
}
}
- }
- if (fields.Contains(ItemFields.ServiceName))
- {
var service = GetService(program);
- if (service != null)
+ var serviceName = service == null ? null : service.Name;
+
+ if (fields.Contains(ItemFields.ServiceName))
{
- dto.ServiceName = service.Name;
+ dto.ServiceName = serviceName;
}
+
+ recordingTuples.Add(new Tuple<BaseItemDto, string, string>(dto, serviceName, program.ExternalId));
}
+
+ await AddRecordingInfo(recordingTuples, CancellationToken.None).ConfigureAwait(false);
}
public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, User user = null)
@@ -2343,7 +2369,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
throw new ResourceNotFoundException();
}
- await provider.Validate(info).ConfigureAwait(false);
+ var configurable = provider as IConfigurableTunerHost;
+ if (configurable != null)
+ {
+ await configurable.Validate(info).ConfigureAwait(false);
+ }
var config = GetConfiguration();
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 4ebc173b5..fb27631e5 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -64,7 +64,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
return list;
}
- private List<TunerHostInfo> GetTunerHosts()
+ protected virtual List<TunerHostInfo> GetTunerHosts()
{
return GetConfiguration().TunerHosts
.Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
index 92a33993a..0a03e60fa 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
@@ -90,7 +90,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await _liveTvManager.SaveTunerHost(new TunerHostInfo
{
Type = HdHomerunHost.DeviceType,
- Url = url
+ Url = url,
+ DataVersion = 1
}).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 0671a9b56..bdfbee521 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -14,13 +14,14 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
- public class HdHomerunHost : BaseTunerHost, ITunerHost
+ public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
private readonly IHttpClient _httpClient;
@@ -47,6 +48,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private const string ChannelIdPrefix = "hdhr_";
+ private string GetChannelId(TunerHostInfo info, Channels i)
+ {
+ var id = ChannelIdPrefix + i.GuideNumber.ToString(CultureInfo.InvariantCulture);
+
+ if (info.DataVersion >= 1)
+ {
+ id += '_' + (i.GuideName ?? string.Empty).GetMD5().ToString("N");
+ }
+
+ return id;
+ }
+
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
{
var options = new HttpRequestOptions
@@ -64,8 +77,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
Name = i.GuideName,
Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
- Id = ChannelIdPrefix + i.GuideNumber.ToString(CultureInfo.InvariantCulture),
- IsFavorite = i.Favorite
+ Id = GetChannelId(info, i),
+ IsFavorite = i.Favorite,
+ TunerHostId = info.Id
});
@@ -82,30 +96,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken)
{
- string model = null;
-
using (var stream = await _httpClient.Get(new HttpRequestOptions()
{
- Url = string.Format("{0}/", GetApiUrl(info, false)),
+ Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
CancellationToken = cancellationToken,
CacheLength = TimeSpan.FromDays(1),
CacheMode = CacheMode.Unconditional,
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds)
}))
{
- using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
- {
- while (!sr.EndOfStream)
- {
- string line = StripXML(sr.ReadLine());
- if (line.StartsWith("Model:")) { model = line.Replace("Model: ", ""); }
- //if (line.StartsWith("Device ID:")) { deviceID = line.Replace("Device ID: ", ""); }
- //if (line.StartsWith("Firmware:")) { firmware = line.Replace("Firmware: ", ""); }
- }
- }
- }
+ var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
- return model;
+ return response.ModelNumber;
+ }
}
public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
@@ -347,6 +350,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return Config.GetConfiguration<EncodingOptions>("encoding");
}
+ private string GetHdHrIdFromChannelId(string channelId)
+ {
+ return channelId.Split('_')[1];
+ }
+
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
{
var list = new List<MediaSourceInfo>();
@@ -355,9 +363,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
return list;
}
- channelId = channelId.Substring(ChannelIdPrefix.Length);
+ var hdhrId = GetHdHrIdFromChannelId(channelId);
- list.Add(GetMediaSource(info, channelId, "native"));
+ list.Add(GetMediaSource(info, hdhrId, "native"));
try
{
@@ -366,12 +374,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
{
- list.Insert(0, GetMediaSource(info, channelId, "heavy"));
+ list.Insert(0, GetMediaSource(info, hdhrId, "heavy"));
- list.Add(GetMediaSource(info, channelId, "internet480"));
- list.Add(GetMediaSource(info, channelId, "internet360"));
- list.Add(GetMediaSource(info, channelId, "internet240"));
- list.Add(GetMediaSource(info, channelId, "mobile"));
+ list.Add(GetMediaSource(info, hdhrId, "internet480"));
+ list.Add(GetMediaSource(info, hdhrId, "internet360"));
+ list.Add(GetMediaSource(info, hdhrId, "internet240"));
+ list.Add(GetMediaSource(info, hdhrId, "mobile"));
}
}
catch (Exception ex)
@@ -400,9 +408,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
throw new ArgumentException("Channel not found");
}
- channelId = channelId.Substring(ChannelIdPrefix.Length);
+ var hdhrId = GetHdHrIdFromChannelId(channelId);
- return GetMediaSource(info, channelId, streamId);
+ return GetMediaSource(info, hdhrId, streamId);
}
public async Task Validate(TunerHostInfo info)
@@ -419,5 +427,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return info.Any(i => i.Status == LiveTvTunerStatus.Available);
}
+
+ public class DiscoverResponse
+ {
+ public string FriendlyName { get; set; }
+ public string ModelNumber { get; set; }
+ public string FirmwareName { get; set; }
+ public string FirmwareVersion { get; set; }
+ public string DeviceID { get; set; }
+ public string DeviceAuth { get; set; }
+ public string BaseURL { get; set; }
+ public string LineupURL { get; set; }
+ }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index f87d4f43f..523f14dfc 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -8,19 +8,17 @@ using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommonIO;
-using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
- public class M3UTunerHost : BaseTunerHost, ITunerHost
+ public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
private readonly IFileSystem _fileSystem;
private readonly IHttpClient _httpClient;
@@ -46,65 +44,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
{
- var urlHash = info.Url.GetMD5().ToString("N");
-
- // Read the file and display it line by line.
- using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
- {
- return GetChannels(reader, urlHash);
- }
- }
-
- private List<M3UChannel> GetChannels(StreamReader reader, string urlHash)
- {
- var channels = new List<M3UChannel>();
-
- string channnelName = null;
- string channelNumber = null;
- string line;
-
- while ((line = reader.ReadLine()) != null)
- {
- line = line.Trim();
- if (string.IsNullOrWhiteSpace(line))
- {
- continue;
- }
-
- if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- 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
- {
- Name = channnelName,
- Number = channelNumber,
- Id = ChannelIdPrefix + urlHash + line.GetMD5().ToString("N"),
- Path = line
- });
-
- channelNumber = null;
- channnelName = null;
- }
- }
- return channels;
+ return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(info.Url, ChannelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
}
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
{
- var list = GetConfiguration().TunerHosts
- .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
+ var list = GetTunerHosts()
.Select(i => new LiveTvTunerInfo()
{
Name = Name,
@@ -125,18 +70,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
return sources.First();
}
- class M3UChannel : ChannelInfo
- {
- public string Path { get; set; }
-
- public M3UChannel()
- {
- }
- }
-
public async Task Validate(TunerHostInfo info)
{
- using (var stream = await GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
+ using (var stream = await new M3uParser(Logger, _fileSystem, _httpClient).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
{
}
@@ -147,15 +83,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
}
- private Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
- {
- if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return _httpClient.Get(info.Url, cancellationToken);
- }
- return Task.FromResult(_fileSystem.OpenRead(info.Url));
- }
-
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
{
var urlHash = info.Url.GetMD5().ToString("N");
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
new file mode 100644
index 000000000..f8f003fa1
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
+{
+ public class M3uParser
+ {
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly IHttpClient _httpClient;
+
+ public M3uParser(ILogger logger, IFileSystem fileSystem, IHttpClient httpClient)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _httpClient = httpClient;
+ }
+
+ public async Task<List<M3UChannel>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
+ {
+ var urlHash = url.GetMD5().ToString("N");
+
+ // Read the file and display it line by line.
+ using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
+ {
+ return GetChannels(reader, urlHash, channelIdPrefix, tunerHostId);
+ }
+ }
+
+ public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
+ {
+ if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ return _httpClient.Get(url, cancellationToken);
+ }
+ return Task.FromResult(_fileSystem.OpenRead(url));
+ }
+
+ private List<M3UChannel> GetChannels(StreamReader reader, string urlHash, string channelIdPrefix, string tunerHostId)
+ {
+ var channels = new List<M3UChannel>();
+ string line;
+ string extInf = "";
+ while ((line = reader.ReadLine()) != null)
+ {
+ line = line.Trim();
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase))
+ {
+ extInf = line.Substring(8).Trim();
+ _logger.Info("Found m3u channel: {0}", extInf);
+ }
+ else if (!string.IsNullOrWhiteSpace(extInf))
+ {
+ var channel = GetChannelnfo(extInf, tunerHostId);
+ channel.Id = channelIdPrefix + urlHash + line.GetMD5().ToString("N");
+ channel.Path = line;
+ channels.Add(channel);
+ extInf = "";
+ }
+ }
+ return channels;
+ }
+ private M3UChannel GetChannelnfo(string extInf, string tunerHostId)
+ {
+ var titleIndex = extInf.LastIndexOf(',');
+ var channel = new M3UChannel();
+ channel.TunerHostId = tunerHostId;
+
+ channel.Number = extInf.Trim().Split(' ')[0] ?? "0";
+ channel.Name = extInf.Substring(titleIndex + 1);
+
+ if(channel.Number == "-1") { channel.Number = "0"; }
+
+ //Check for channel number with the format from SatIp
+ int number;
+ var numberIndex = channel.Name.IndexOf('.');
+ if (numberIndex > 0)
+ {
+ if (int.TryParse(channel.Name.Substring(0, numberIndex), out number))
+ {
+ channel.Number = number.ToString();
+ channel.Name = channel.Name.Substring(numberIndex + 1);
+ }
+ }
+ channel.ImageUrl = FindProperty("tvg-logo", extInf, null);
+ channel.Number = FindProperty("tvg-id", extInf, channel.Number);
+ channel.Number = FindProperty("channel-id", extInf, channel.Number);
+ channel.Name = FindProperty("tvg-name", extInf, channel.Name);
+ channel.Name = FindProperty("tvg-id", extInf, channel.Name);
+ return channel;
+
+ }
+ private string FindProperty(string property, string properties, string defaultResult = "")
+ {
+ var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
+ var matches = reg.Matches(properties);
+ foreach (Match match in matches)
+ {
+ if (match.Groups[1].Value == property)
+ {
+ return match.Groups[2].Value;
+ }
+ }
+ return defaultResult;
+ }
+ }
+
+
+ public class M3UChannel : ChannelInfo
+ {
+ public string Path { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
index 08c42bb46..6781e498a 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using System.Xml;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
@@ -13,6 +17,7 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
{
@@ -24,14 +29,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
private readonly ILiveTvManager _liveTvManager;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private readonly IHttpClient _httpClient;
+ private readonly IJsonSerializer _json;
- public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient)
+ public static SatIpDiscovery Current;
+
+ private readonly List<TunerHostInfo> _discoveredHosts = new List<TunerHostInfo>();
+
+ public List<TunerHostInfo> DiscoveredHosts
+ {
+ get { return _discoveredHosts.ToList(); }
+ }
+
+ public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json)
{
_deviceDiscovery = deviceDiscovery;
_config = config;
_logger = logger;
_liveTvManager = liveTvManager;
_httpClient = httpClient;
+ _json = json;
+ Current = this;
}
public void Run()
@@ -42,7 +59,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
{
string st = null;
- if (e.Headers.TryGetValue("ST", out st) && string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase))
+ string nt = null;
+ e.Headers.TryGetValue("ST", out st);
+ e.Headers.TryGetValue("NT", out nt);
+
+ if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase))
{
string location;
if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location))
@@ -61,26 +83,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
try
{
- var options = GetConfiguration();
-
- //if (options.TunerHosts.Any(i =>
- // string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) &&
- // UriEquals(i.Url, url)))
- //{
- // return;
- //}
+ if (_discoveredHosts.Any(i => string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(location, i.Url, StringComparison.OrdinalIgnoreCase)))
+ {
+ return;
+ }
- //// Strip off the port
- //url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/');
+ _logger.Debug("Will attempt to add SAT device {0}", location);
+ var info = await GetInfo(location, CancellationToken.None).ConfigureAwait(false);
- //await TestUrl(url).ConfigureAwait(false);
+ _discoveredHosts.Add(info);
+ }
+ catch (OperationCanceledException)
+ {
- //await _liveTvManager.SaveTunerHost(new TunerHostInfo
- //{
- // Type = SatIpHost.DeviceType,
- // Url = url
+ }
+ catch (NotImplementedException)
+ {
- //}).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -92,43 +111,141 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
}
}
- private async Task TestUrl(string url)
+ public void Dispose()
{
- // Test it by pulling down the lineup
- using (await _httpClient.Get(new HttpRequestOptions
+ }
+
+ public async Task<SatIpTunerHostInfo> GetInfo(string url, CancellationToken cancellationToken)
+ {
+ var result = new SatIpTunerHostInfo
{
- Url = string.Format("{0}/lineup.json", url),
- CancellationToken = CancellationToken.None
- }))
+ Url = url,
+ IsEnabled = true,
+ Type = SatIpHost.DeviceType,
+ Tuners = 1,
+ TunersAvailable = 1
+ };
+
+ using (var stream = await _httpClient.Get(url, cancellationToken).ConfigureAwait(false))
+ {
+ using (var streamReader = new StreamReader(stream))
+ {
+ // Use XmlReader for best performance
+ using (var reader = XmlReader.Create(streamReader))
+ {
+ reader.MoveToContent();
+
+ // Loop through each element
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "device":
+ using (var subtree = reader.ReadSubtree())
+ {
+ FillFromDeviceNode(result, subtree);
+ }
+ break;
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(result.Id))
{
+ throw new NotImplementedException();
}
- }
- private bool UriEquals(string savedUri, string location)
- {
- return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase);
- }
+ // Device hasn't implemented an m3u list
+ if (string.IsNullOrWhiteSpace(result.M3UUrl))
+ {
+ result.IsEnabled = false;
+ }
- private string NormalizeUrl(string url)
- {
- if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ else if (!result.M3UUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- url = "http://" + url;
+ var fullM3uUrl = url.Substring(0, url.LastIndexOf('/'));
+ result.M3UUrl = fullM3uUrl + "/" + result.M3UUrl.TrimStart('/');
}
- url = url.TrimEnd('/');
+ _logger.Debug("SAT device result: {0}", _json.SerializeToString(result));
- // Strip off the port
- return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped);
+ return result;
}
- private LiveTvOptions GetConfiguration()
+ private void FillFromDeviceNode(SatIpTunerHostInfo info, XmlReader reader)
{
- return _config.GetConfiguration<LiveTvOptions>("livetv");
- }
+ reader.MoveToContent();
- public void Dispose()
- {
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.LocalName)
+ {
+ case "UDN":
+ {
+ info.Id = reader.ReadElementContentAsString();
+ break;
+ }
+
+ case "friendlyName":
+ {
+ info.FriendlyName = reader.ReadElementContentAsString();
+ break;
+ }
+
+ case "satip:X_SATIPCAP":
+ case "X_SATIPCAP":
+ {
+ // <satip:X_SATIPCAP xmlns:satip="urn:ses-com:satip">DVBS2-2</satip:X_SATIPCAP>
+ var value = reader.ReadElementContentAsString() ?? string.Empty;
+ var parts = value.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length == 2)
+ {
+ int intValue;
+ if (int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out intValue))
+ {
+ info.TunersAvailable = intValue;
+ }
+
+ if (int.TryParse(parts[0].Substring(parts[0].Length - 1), NumberStyles.Any, CultureInfo.InvariantCulture, out intValue))
+ {
+ info.Tuners = intValue;
+ }
+ }
+ break;
+ }
+
+ case "satip:X_SATIPM3U":
+ case "X_SATIPM3U":
+ {
+ // <satip:X_SATIPM3U xmlns:satip="urn:ses-com:satip">/channellist.lua?select=m3u</satip:X_SATIPM3U>
+ info.M3UUrl = reader.ReadElementContentAsString();
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
}
}
+
+ public class SatIpTunerHostInfo : TunerHostInfo
+ {
+ public int Tuners { get; set; }
+ public int TunersAvailable { get; set; }
+ public string M3UUrl { get; set; }
+ public string FriendlyName { get; set; }
+ }
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
index 205cdf74e..181169e9a 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
@@ -1,45 +1,171 @@
-namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
{
- public class SatIpHost /*: BaseTunerHost*/
+ public class SatIpHost : BaseTunerHost, ITunerHost
{
- //public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
- // : base(config, logger, jsonSerializer, mediaEncoder)
- //{
- //}
+ private readonly IFileSystem _fileSystem;
+ private readonly IHttpClient _httpClient;
+
+ public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
+ : base(config, logger, jsonSerializer, mediaEncoder)
+ {
+ _fileSystem = fileSystem;
+ _httpClient = httpClient;
+ }
+
+ private const string ChannelIdPrefix = "sat_";
+
+ protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
+ {
+ var satInfo = (SatIpTunerHostInfo) tuner;
- //protected override Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
- //{
- // throw new NotImplementedException();
- //}
+ return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(satInfo.M3UUrl, ChannelIdPrefix, tuner.Id, cancellationToken).ConfigureAwait(false);
+ }
public static string DeviceType
{
get { return "satip"; }
}
- //public override string Type
- //{
- // get { return DeviceType; }
- //}
+ public override string Type
+ {
+ get { return DeviceType; }
+ }
+
+ protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
+ {
+ var urlHash = tuner.Url.GetMD5().ToString("N");
+ var prefix = ChannelIdPrefix + urlHash;
+ if (!channelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ var channels = await GetChannels(tuner, true, cancellationToken).ConfigureAwait(false);
+ var m3uchannels = channels.Cast<M3UChannel>();
+ var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase));
+ if (channel != null)
+ {
+ var path = channel.Path;
+ MediaProtocol protocol = MediaProtocol.File;
+ if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ protocol = MediaProtocol.Http;
+ }
+ else if (path.StartsWith("rtmp", StringComparison.OrdinalIgnoreCase))
+ {
+ protocol = MediaProtocol.Rtmp;
+ }
+ else if (path.StartsWith("rtsp", StringComparison.OrdinalIgnoreCase))
+ {
+ protocol = MediaProtocol.Rtsp;
+ }
+
+ var mediaSource = new MediaSourceInfo
+ {
+ Path = channel.Path,
+ Protocol = protocol,
+ MediaStreams = new List<MediaStream>
+ {
+ new MediaStream
+ {
+ Type = MediaStreamType.Video,
+ // Set the index to -1 because we don't know the exact index of the video stream within the container
+ Index = -1,
+ IsInterlaced = true
+ },
+ new MediaStream
+ {
+ Type = MediaStreamType.Audio,
+ // Set the index to -1 because we don't know the exact index of the audio stream within the container
+ Index = -1
+
+ }
+ },
+ RequiresOpening = false,
+ RequiresClosing = false
+ };
+
+ return new List<MediaSourceInfo> { mediaSource };
+ }
+ return new List<MediaSourceInfo> { };
+ }
+
+ protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
+ {
+ var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false);
+
+ return sources.First();
+ }
+
+ protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
+ {
+ var updatedInfo = await SatIpDiscovery.Current.GetInfo(tuner.Url, cancellationToken).ConfigureAwait(false);
- //protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
- //{
- // throw new NotImplementedException();
- //}
+ return updatedInfo.TunersAvailable > 0;
+ }
+
+ protected override bool IsValidChannelId(string channelId)
+ {
+ return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
+ }
- //protected override Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
- //{
- // throw new NotImplementedException();
- //}
+ protected override List<TunerHostInfo> GetTunerHosts()
+ {
+ return SatIpDiscovery.Current.DiscoveredHosts;
+ }
- //protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
- //{
- // throw new NotImplementedException();
- //}
+ public string Name
+ {
+ get { return "Sat IP"; }
+ }
- //protected override bool IsValidChannelId(string channelId)
- //{
- // throw new NotImplementedException();
- //}
+ public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
+ {
+ var list = GetTunerHosts()
+ .SelectMany(i => GetTunerInfos(i, cancellationToken))
+ .ToList();
+
+ return Task.FromResult(list);
+ }
+
+ public List<LiveTvTunerInfo> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
+ {
+ var satInfo = (SatIpTunerHostInfo) info;
+
+ var list = new List<LiveTvTunerInfo>();
+
+ for (var i = 0; i < satInfo.Tuners; i++)
+ {
+ list.Add(new LiveTvTunerInfo
+ {
+ Name = satInfo.FriendlyName ?? Name,
+ SourceType = Type,
+ Status = LiveTvTunerStatus.Available,
+ Id = info.Url.GetMD5().ToString("N") + i.ToString(CultureInfo.InvariantCulture),
+ Url = info.Url
+ });
+ }
+
+ return list;
+ }
}
}