aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/LiveTv
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv')
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs4
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs26
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs726
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs36
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs126
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs2
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs32
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs165
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs44
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs219
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs371
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs2
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs2
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs2
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs30
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs183
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs9
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/ChannelScan.cs105
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs88
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs140
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs149
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs688
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs251
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs169
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs8
25 files changed, 2997 insertions, 580 deletions
diff --git a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
index 24d38a63e..23560b1aa 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
@@ -41,7 +41,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase));
- if (service != null)
+ if (service != null && !item.HasImage(ImageType.Primary))
{
try
{
@@ -77,7 +77,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
get { return 0; }
}
- public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService)
+ public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
{
return GetSupportedImages(item).Any(i => !item.HasImage(i));
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index d33b2c51d..b21aa904b 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -23,6 +23,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_fileSystem = fileSystem;
}
+ public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile)
+ {
+ return targetFile;
+ }
+
public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
var httpRequestOptions = new HttpRequestOptions()
@@ -40,14 +45,27 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
onStarted();
- _logger.Info("Copying recording stream to file stream");
+ _logger.Info("Copying recording stream to file {0}", targetFile);
- var durationToken = new CancellationTokenSource(duration);
- var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ if (mediaSource.RunTimeTicks.HasValue)
+ {
+ // The media source already has a fixed duration
+ // But add another stop 1 minute later just in case the recording gets stuck for any reason
+ var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1)));
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ }
+ else
+ {
+ // The media source if infinite so we need to handle stopping ourselves
+ var durationToken = new CancellationTokenSource(duration);
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ }
- await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken).ConfigureAwait(false);
+ await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
}
}
+
+ _logger.Info("Recording completed to file {0}", targetFile);
}
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 60ff23b04..8f56554f1 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -26,13 +26,16 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommonIO;
+using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Power;
using Microsoft.Win32;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
- public class EmbyTV : ILiveTvService, IHasRegistrationInfo, IDisposable
+ public class EmbyTV : ILiveTvService, ISupportsNewTimerIds, IHasRegistrationInfo, IDisposable
{
private readonly IApplicationHost _appHpst;
private readonly ILogger _logger;
@@ -40,7 +43,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
private readonly IServerConfigurationManager _config;
private readonly IJsonSerializer _jsonSerializer;
- private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
private readonly TimerManager _timerProvider;
@@ -56,6 +58,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public static EmbyTV Current;
+ public event EventHandler DataSourceChanged;
+ public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
+
+ private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
+ new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
+
public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement)
{
Current = this;
@@ -74,10 +82,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_liveTvManager = (LiveTvManager)liveTvManager;
_jsonSerializer = jsonSerializer;
- _recordingProvider = new ItemDataProvider<RecordingInfo>(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase));
_seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
_timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger);
_timerProvider.TimerFired += _timerProvider_TimerFired;
+
+ _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+ }
+
+ private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+ {
+ if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
+ {
+ OnRecordingFoldersChanged();
+ }
}
public void Start()
@@ -85,6 +102,124 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_timerProvider.RestartTimers();
SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
+ CreateRecordingFolders();
+ }
+
+ private void OnRecordingFoldersChanged()
+ {
+ CreateRecordingFolders();
+ }
+
+ internal void CreateRecordingFolders()
+ {
+ try
+ {
+ CreateRecordingFoldersInternal();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error creating recording folders", ex);
+ }
+ }
+
+ internal void CreateRecordingFoldersInternal()
+ {
+ var recordingFolders = GetRecordingFolders();
+
+ var virtualFolders = _libraryManager.GetVirtualFolders()
+ .ToList();
+
+ var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
+
+ var pathsAdded = new List<string>();
+
+ foreach (var recordingFolder in recordingFolders)
+ {
+ var pathsToCreate = recordingFolder.Locations
+ .Where(i => !allExistingPaths.Contains(i, StringComparer.OrdinalIgnoreCase))
+ .ToList();
+
+ if (pathsToCreate.Count == 0)
+ {
+ continue;
+ }
+
+ try
+ {
+ _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, pathsToCreate.ToArray(), true);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error creating virtual folder", ex);
+ }
+
+ pathsAdded.AddRange(pathsToCreate);
+ }
+
+ var config = GetConfiguration();
+
+ var pathsToRemove = config.MediaLocationsCreated
+ .Except(recordingFolders.SelectMany(i => i.Locations))
+ .ToList();
+
+ if (pathsAdded.Count > 0 || pathsToRemove.Count > 0)
+ {
+ pathsAdded.InsertRange(0, config.MediaLocationsCreated);
+ config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ _config.SaveConfiguration("livetv", config);
+ }
+
+ foreach (var path in pathsToRemove)
+ {
+ RemovePathFromLibrary(path);
+ }
+ }
+
+ private void RemovePathFromLibrary(string path)
+ {
+ _logger.Debug("Removing path from library: {0}", path);
+
+ var requiresRefresh = false;
+ var virtualFolders = _libraryManager.GetVirtualFolders()
+ .ToList();
+
+ foreach (var virtualFolder in virtualFolders)
+ {
+ if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (virtualFolder.Locations.Count == 1)
+ {
+ // remove entire virtual folder
+ try
+ {
+ _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error removing virtual folder", ex);
+ }
+ }
+ else
+ {
+ try
+ {
+ _libraryManager.RemoveMediaPath(virtualFolder.Name, path);
+ requiresRefresh = true;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error removing media path", ex);
+ }
+ }
+ }
+
+ if (requiresRefresh)
+ {
+ _libraryManager.ValidateMediaLibrary(new Progress<Double>(), CancellationToken.None);
+ }
}
void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
@@ -97,13 +232,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- public event EventHandler DataSourceChanged;
-
- public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
-
- private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
- new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
-
public string Name
{
get { return "Emby"; }
@@ -114,6 +242,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
}
+ private string DefaultRecordingPath
+ {
+ get
+ {
+ return Path.Combine(DataPath, "recordings");
+ }
+ }
+
+ private string RecordingPath
+ {
+ get
+ {
+ var path = GetConfiguration().RecordingPath;
+
+ return string.IsNullOrWhiteSpace(path)
+ ? DefaultRecordingPath
+ : path;
+ }
+ }
+
public string HomePageUrl
{
get { return "http://emby.media"; }
@@ -234,6 +382,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return list;
}
+ public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken)
+ {
+ var list = new List<ChannelInfo>();
+
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
+ {
+ try
+ {
+ var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
+
+ list.AddRange(channels);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting channels", ex);
+ }
+ }
+
+ return list
+ .Where(i => IsListingProviderEnabledForTuner(listingsProvider, i.TunerHostId))
+ .ToList();
+ }
+
public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
{
return GetChannelsAsync(false, cancellationToken);
@@ -280,59 +451,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return Task.FromResult(true);
}
- public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
+ public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
{
- var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase));
- if (remove != null)
- {
- if (!string.IsNullOrWhiteSpace(remove.TimerId))
- {
- var enableDelay = _activeRecordings.ContainsKey(remove.TimerId);
-
- CancelTimerInternal(remove.TimerId);
-
- if (enableDelay)
- {
- // A hack yes, but need to make sure the file is closed before attempting to delete it
- await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
- }
- }
-
- if (!string.IsNullOrWhiteSpace(remove.Path))
- {
- try
- {
- _fileSystem.DeleteFile(remove.Path);
- }
- catch (DirectoryNotFoundException)
- {
+ return Task.FromResult(true);
+ }
- }
- catch (FileNotFoundException)
- {
+ public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ {
+ return CreateTimer(info, cancellationToken);
+ }
- }
- catch (Exception ex)
- {
- _logger.ErrorException("Error deleting recording file {0}", ex, remove.Path);
- }
- }
- _recordingProvider.Delete(remove);
- }
- else
- {
- throw new ResourceNotFoundException("Recording not found: " + recordingId);
- }
+ public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+ {
+ return CreateSeriesTimer(info, cancellationToken);
}
- public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
{
info.Id = Guid.NewGuid().ToString("N");
_timerProvider.Add(info);
- return Task.FromResult(0);
+ return Task.FromResult(info.Id);
}
- public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+ public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
{
info.Id = Guid.NewGuid().ToString("N");
@@ -362,6 +503,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_seriesTimerProvider.Add(info);
await UpdateTimersForSeriesTimer(epgData, info, false).ConfigureAwait(false);
+
+ return info.Id;
}
public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
@@ -424,29 +567,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
{
- var recordings = _recordingProvider.GetAll().ToList();
- var updated = false;
-
- foreach (var recording in recordings)
- {
- if (recording.Status == RecordingStatus.InProgress)
- {
- if (string.IsNullOrWhiteSpace(recording.TimerId) || !_activeRecordings.ContainsKey(recording.TimerId))
- {
- recording.Status = RecordingStatus.Cancelled;
- recording.DateLastUpdated = DateTime.UtcNow;
- _recordingProvider.Update(recording);
- updated = true;
- }
- }
- }
-
- if (updated)
- {
- recordings = _recordingProvider.GetAll().ToList();
- }
-
- return recordings;
+ return new List<RecordingInfo>();
}
public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
@@ -462,9 +583,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
- RecordAnyChannel = false,
- RecordAnyTime = false,
- RecordNewOnly = false
+ RecordAnyChannel = true,
+ RecordAnyTime = true,
+ RecordNewOnly = false,
+
+ Days = new List<DayOfWeek>
+ {
+ DayOfWeek.Sunday,
+ DayOfWeek.Monday,
+ DayOfWeek.Tuesday,
+ DayOfWeek.Wednesday,
+ DayOfWeek.Thursday,
+ DayOfWeek.Friday,
+ DayOfWeek.Saturday
+ }
};
if (program != null)
@@ -528,7 +660,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_logger.Debug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
- var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, channel.Name, startDateUtc, endDateUtc, cancellationToken)
+ var channelMappings = GetChannelMappings(provider.Item2);
+ var channelNumber = channel.Number;
+ string mappedChannelNumber;
+ if (channelMappings.TryGetValue(channelNumber, out mappedChannelNumber))
+ {
+ _logger.Debug("Found mapped channel on provider {0}. Tuner channel number: {1}, Mapped channel number: {2}", provider.Item1.Name, channelNumber, mappedChannelNumber);
+ channelNumber = mappedChannelNumber;
+ }
+
+ var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channelNumber, channel.Name, startDateUtc, endDateUtc, cancellationToken)
.ConfigureAwait(false);
var list = programs.ToList();
@@ -550,6 +691,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return new List<ProgramInfo>();
}
+ private Dictionary<string, string> GetChannelMappings(ListingsProviderInfo info)
+ {
+ var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var mapping in info.ChannelMappings)
+ {
+ dict[mapping.Name] = mapping.Value;
+ }
+
+ return dict;
+ }
+
private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
{
return GetConfiguration().ListingProviders
@@ -591,7 +744,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new ApplicationException("Tuner not found.");
}
- private async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
+ private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
{
_logger.Info("Streaming Channel " + channelId);
@@ -599,7 +752,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
try
{
- return await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+ var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+
+ return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2);
}
catch (Exception e)
{
@@ -693,6 +848,106 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
+ private string GetRecordingPath(TimerInfo timer, ProgramInfo info)
+ {
+ var recordPath = RecordingPath;
+ var config = GetConfiguration();
+
+ if (info.IsMovie)
+ {
+ var customRecordingPath = config.MovieRecordingPath;
+ var allowSubfolder = true;
+ if (!string.IsNullOrWhiteSpace(customRecordingPath))
+ {
+ allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase);
+ recordPath = customRecordingPath;
+ }
+
+ if (allowSubfolder && config.EnableRecordingSubfolders)
+ {
+ recordPath = Path.Combine(recordPath, "Movies");
+ }
+
+ var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
+ if (info.ProductionYear.HasValue)
+ {
+ folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+ }
+ recordPath = Path.Combine(recordPath, folderName);
+ }
+ else if (info.IsSeries)
+ {
+ var customRecordingPath = config.SeriesRecordingPath;
+ var allowSubfolder = true;
+ if (!string.IsNullOrWhiteSpace(customRecordingPath))
+ {
+ allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase);
+ recordPath = customRecordingPath;
+ }
+
+ if (allowSubfolder && config.EnableRecordingSubfolders)
+ {
+ recordPath = Path.Combine(recordPath, "Series");
+ }
+
+ var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
+ var folderNameWithYear = folderName;
+ if (info.ProductionYear.HasValue)
+ {
+ folderNameWithYear += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+ }
+
+ if (Directory.Exists(Path.Combine(recordPath, folderName)))
+ {
+ recordPath = Path.Combine(recordPath, folderName);
+ }
+ else
+ {
+ recordPath = Path.Combine(recordPath, folderNameWithYear);
+ }
+
+ if (info.SeasonNumber.HasValue)
+ {
+ folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
+ recordPath = Path.Combine(recordPath, folderName);
+ }
+ }
+ else if (info.IsKids)
+ {
+ if (config.EnableRecordingSubfolders)
+ {
+ recordPath = Path.Combine(recordPath, "Kids");
+ }
+
+ var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
+ if (info.ProductionYear.HasValue)
+ {
+ folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+ }
+ recordPath = Path.Combine(recordPath, folderName);
+ }
+ else if (info.IsSports)
+ {
+ if (config.EnableRecordingSubfolders)
+ {
+ recordPath = Path.Combine(recordPath, "Sports");
+ }
+ recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
+ }
+ else
+ {
+ if (config.EnableRecordingSubfolders)
+ {
+ recordPath = Path.Combine(recordPath, "Other");
+ }
+ recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
+ }
+
+ var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
+
+ return Path.Combine(recordPath, recordingFileName);
+ }
+
private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
{
if (timer == null)
@@ -722,161 +977,102 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId));
}
- var recordPath = RecordingPath;
-
- if (info.IsMovie)
- {
- recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim());
- }
- else if (info.IsSeries)
- {
- recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim());
- }
- else if (info.IsKids)
- {
- recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim());
- }
- else if (info.IsSports)
- {
- recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim());
- }
- else
- {
- recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim());
- }
-
- var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
-
- recordPath = Path.Combine(recordPath, recordingFileName);
-
- var recordingId = info.Id.GetMD5().ToString("N");
- var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase));
-
- if (recording == null)
- {
- recording = new RecordingInfo
- {
- ChannelId = info.ChannelId,
- Id = recordingId,
- StartDate = info.StartDate,
- EndDate = info.EndDate,
- Genres = info.Genres,
- IsKids = info.IsKids,
- IsLive = info.IsLive,
- IsMovie = info.IsMovie,
- IsHD = info.IsHD,
- IsNews = info.IsNews,
- IsPremiere = info.IsPremiere,
- IsSeries = info.IsSeries,
- IsSports = info.IsSports,
- IsRepeat = !info.IsPremiere,
- Name = info.Name,
- EpisodeTitle = info.EpisodeTitle,
- ProgramId = info.Id,
- ImagePath = info.ImagePath,
- ImageUrl = info.ImageUrl,
- OriginalAirDate = info.OriginalAirDate,
- Status = RecordingStatus.Scheduled,
- Overview = info.Overview,
- SeriesTimerId = timer.SeriesTimerId,
- TimerId = timer.Id,
- ShowId = info.ShowId
- };
- _recordingProvider.AddOrUpdate(recording);
- }
+ var recordPath = GetRecordingPath(timer, info);
+ var recordingStatus = RecordingStatus.New;
+ var isResourceOpen = false;
+ SemaphoreSlim semaphore = null;
try
{
var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
+ isResourceOpen = true;
+ semaphore = result.Item3;
var mediaStreamInfo = result.Item1;
- var isResourceOpen = true;
- // Unfortunately due to the semaphore we have to have a nested try/finally
- try
- {
- // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
- //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
+ // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
+ //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
- var duration = recordingEndDate - DateTime.UtcNow;
+ var recorder = await GetRecorder().ConfigureAwait(false);
- var recorder = await GetRecorder().ConfigureAwait(false);
+ recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath);
+ recordPath = EnsureFileUnique(recordPath, timer.Id);
- if (recorder is EncodedRecorder)
- {
- recordPath = Path.ChangeExtension(recordPath, ".mp4");
- }
- recordPath = EnsureFileUnique(recordPath, timer.Id);
- _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath));
- activeRecordingInfo.Path = recordPath;
+ _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
+ _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath));
+ activeRecordingInfo.Path = recordPath;
- _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
+ var duration = recordingEndDate - DateTime.UtcNow;
- recording.Path = recordPath;
- recording.Status = RecordingStatus.InProgress;
- recording.DateLastUpdated = DateTime.UtcNow;
- _recordingProvider.AddOrUpdate(recording);
+ _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
- _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+ _logger.Info("Writing file to path: " + recordPath);
+ _logger.Info("Opening recording stream from tuner provider");
- _logger.Info("Writing file to path: " + recordPath);
- _logger.Info("Opening recording stream from tuner provider");
+ Action onStarted = () =>
+ {
+ timer.Status = RecordingStatus.InProgress;
+ _timerProvider.AddOrUpdate(timer, false);
- Action onStarted = () =>
- {
- result.Item2.Release();
- isResourceOpen = false;
- };
+ result.Item3.Release();
+ isResourceOpen = false;
+ };
- await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
+ var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration);
- recording.Status = RecordingStatus.Completed;
- _logger.Info("Recording completed: {0}", recordPath);
- }
- finally
+ // If it supports supplying duration via url
+ if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase))
{
- if (isResourceOpen)
- {
- result.Item2.Release();
- }
-
- _libraryMonitor.ReportFileSystemChangeComplete(recordPath, false);
+ mediaStreamInfo.Path = pathWithDuration;
+ mediaStreamInfo.RunTimeTicks = duration.Ticks;
}
+
+ await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
+
+ recordingStatus = RecordingStatus.Completed;
+ _logger.Info("Recording completed: {0}", recordPath);
}
catch (OperationCanceledException)
{
_logger.Info("Recording stopped: {0}", recordPath);
- recording.Status = RecordingStatus.Completed;
+ recordingStatus = RecordingStatus.Completed;
}
catch (Exception ex)
{
_logger.ErrorException("Error recording to {0}", ex, recordPath);
- recording.Status = RecordingStatus.Error;
+ recordingStatus = RecordingStatus.Error;
}
finally
{
+ if (isResourceOpen && semaphore != null)
+ {
+ semaphore.Release();
+ }
+
+ _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
+
ActiveRecordingInfo removed;
_activeRecordings.TryRemove(timer.Id, out removed);
}
- recording.DateLastUpdated = DateTime.UtcNow;
- _recordingProvider.AddOrUpdate(recording);
-
- if (recording.Status == RecordingStatus.Completed)
+ if (recordingStatus == RecordingStatus.Completed)
{
- OnSuccessfulRecording(recording);
+ timer.Status = RecordingStatus.Completed;
_timerProvider.Delete(timer);
+
+ OnSuccessfulRecording(info.IsSeries, recordPath);
}
else if (DateTime.UtcNow < timer.EndDate)
{
const int retryIntervalSeconds = 60;
_logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
- _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds));
+ timer.Status = RecordingStatus.New;
+ timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds);
+ _timerProvider.AddOrUpdate(timer);
}
else
{
_timerProvider.Delete(timer);
- _recordingProvider.Delete(recording);
}
}
@@ -916,24 +1112,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
private async Task<IRecorder> GetRecorder()
{
- if (GetConfiguration().EnableRecordingEncoding)
+ var config = GetConfiguration();
+
+ if (config.EnableRecordingEncoding)
{
var regInfo = await _security.GetRegistrationStatus("embytvrecordingconversion").ConfigureAwait(false);
if (regInfo.IsValid)
{
- return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
+ return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, config, _httpClient);
}
}
return new DirectRecorder(_logger, _httpClient, _fileSystem);
}
- private async void OnSuccessfulRecording(RecordingInfo recording)
+ private async void OnSuccessfulRecording(bool isSeries, string path)
{
if (GetConfiguration().EnableAutoOrganize)
{
- if (recording.IsSeries)
+ if (isSeries)
{
try
{
@@ -943,12 +1141,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
- var result = await organize.OrganizeEpisodeFile(recording.Path, CancellationToken.None).ConfigureAwait(false);
-
- if (result.Status == FileSortingStatus.Success)
- {
- _recordingProvider.Delete(recording);
- }
+ var result = await organize.OrganizeEpisodeFile(path, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -972,18 +1165,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks);
}
- private string RecordingPath
- {
- get
- {
- var path = GetConfiguration().RecordingPath;
-
- return string.IsNullOrWhiteSpace(path)
- ? Path.Combine(DataPath, "recordings")
- : path;
- }
- }
-
private LiveTvOptions GetConfiguration()
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
@@ -991,7 +1172,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers)
{
- var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList();
+ var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList();
var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
@@ -1005,7 +1186,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
if (deleteInvalidTimers)
{
- var allTimers = GetTimersForSeries(seriesTimer, epgData, new List<RecordingInfo>())
+ var allTimers = GetTimersForSeries(seriesTimer, epgData, false)
.Select(i => i.Id)
.ToList();
@@ -1021,7 +1202,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms, IReadOnlyList<RecordingInfo> currentRecordings)
+ private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer,
+ IEnumerable<ProgramInfo> allPrograms,
+ bool filterByCurrentRecordings)
{
if (seriesTimer == null)
{
@@ -1031,28 +1214,80 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
throw new ArgumentNullException("allPrograms");
}
- if (currentRecordings == null)
- {
- throw new ArgumentNullException("currentRecordings");
- }
// Exclude programs that have already ended
allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow);
allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
- var recordingShowIds = currentRecordings.Select(i => i.ProgramId).Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
-
- allPrograms = allPrograms.Where(i => !recordingShowIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase));
+ if (filterByCurrentRecordings)
+ {
+ allPrograms = allPrograms.Where(i => !IsProgramAlreadyInLibrary(i));
+ }
return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
}
+ private bool IsProgramAlreadyInLibrary(ProgramInfo program)
+ {
+ if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
+ {
+ var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Series).Name },
+ Name = program.Name
+
+ }).Select(i => i.ToString("N")).ToArray();
+
+ if (seriesIds.Length == 0)
+ {
+ return false;
+ }
+
+ if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue)
+ {
+ var result = _libraryManager.GetItemsResult(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Episode).Name },
+ ParentIndexNumber = program.SeasonNumber.Value,
+ IndexNumber = program.EpisodeNumber.Value,
+ AncestorIds = seriesIds,
+ ExcludeLocationTypes = new[] { LocationType.Virtual }
+ });
+
+ if (result.TotalRecordCount > 0)
+ {
+ return true;
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(program.EpisodeTitle))
+ {
+ var result = _libraryManager.GetItemsResult(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Episode).Name },
+ Name = program.EpisodeTitle,
+ AncestorIds = seriesIds,
+ ExcludeLocationTypes = new[] { LocationType.Virtual }
+ });
+
+ if (result.TotalRecordCount > 0)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
{
if (!seriesTimer.RecordAnyTime)
{
allPrograms = allPrograms.Where(epg => Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - epg.StartDate.TimeOfDay.Ticks) < TimeSpan.FromMinutes(5).Ticks);
+
+ allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek));
}
if (seriesTimer.RecordNewOnly)
@@ -1065,8 +1300,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
}
- allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek));
-
if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
{
_logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
@@ -1132,6 +1365,47 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
});
}
+ public List<VirtualFolderInfo> GetRecordingFolders()
+ {
+ var list = new List<VirtualFolderInfo>();
+
+ var defaultFolder = RecordingPath;
+ var defaultName = "Recordings";
+
+ if (Directory.Exists(defaultFolder))
+ {
+ list.Add(new VirtualFolderInfo
+ {
+ Locations = new List<string> { defaultFolder },
+ Name = defaultName
+ });
+ }
+
+ var customPath = GetConfiguration().MovieRecordingPath;
+ if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+ {
+ list.Add(new VirtualFolderInfo
+ {
+ Locations = new List<string> { customPath },
+ Name = "Recorded Movies",
+ CollectionType = CollectionType.Movies
+ });
+ }
+
+ customPath = GetConfiguration().SeriesRecordingPath;
+ if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+ {
+ list.Add(new VirtualFolderInfo
+ {
+ Locations = new List<string> { customPath },
+ Name = "Recorded Series",
+ CollectionType = CollectionType.TvShows
+ });
+ }
+
+ return list;
+ }
+
class ActiveRecordingInfo
{
public string Path { get; set; }
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs
new file mode 100644
index 000000000..675fca325
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs
@@ -0,0 +1,36 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Security;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ public class EmbyTVRegistration : IRequiresRegistration
+ {
+ private readonly ISecurityManager _securityManager;
+
+ public static EmbyTVRegistration Instance;
+
+ public EmbyTVRegistration(ISecurityManager securityManager)
+ {
+ _securityManager = securityManager;
+ Instance = this;
+ }
+
+ private bool? _isXmlTvEnabled;
+
+ public Task LoadRegistrationInfoAsync()
+ {
+ _isXmlTvEnabled = null;
+ return Task.FromResult(true);
+ }
+
+ public async Task<bool> EnableXmlTv()
+ {
+ if (!_isXmlTvEnabled.HasValue)
+ {
+ var info = await _securityManager.GetRegistrationStatus("xmltv").ConfigureAwait(false);
+ _isXmlTvEnabled = info.IsValid;
+ }
+ return _isXmlTvEnabled.Value;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 69cc8ebf7..5e428e6f0 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -8,10 +8,13 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommonIO;
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
@@ -21,8 +24,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
+ private readonly IHttpClient _httpClient;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IApplicationPaths _appPaths;
+ private readonly IServerApplicationPaths _appPaths;
+ private readonly LiveTvOptions _liveTvOptions;
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
@@ -30,17 +35,99 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
private readonly IJsonSerializer _json;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
- public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IApplicationPaths appPaths, IJsonSerializer json)
+ public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, LiveTvOptions liveTvOptions, IHttpClient httpClient)
{
_logger = logger;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
_json = json;
+ _liveTvOptions = liveTvOptions;
+ _httpClient = httpClient;
+ }
+
+ public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile)
+ {
+ return Path.ChangeExtension(targetFile, ".mp4");
}
public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
+ var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts");
+
+ try
+ {
+ await RecordInternal(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ finally
+ {
+ try
+ {
+ File.Delete(tempfile);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error deleting recording temp file", ex);
+ }
+ }
+ }
+
+ public async Task RecordInternal(MediaSourceInfo mediaSource, string tempFile, string targetFile, TimeSpan duration, 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");
+
+ Directory.CreateDirectory(Path.GetDirectoryName(tempFile));
+
+ using (var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ //onStarted();
+
+ _logger.Info("Copying recording stream to file {0}", tempFile);
+
+ var bufferMs = 5000;
+
+ if (mediaSource.RunTimeTicks.HasValue)
+ {
+ // The media source already has a fixed duration
+ // But add another stop 1 minute later just in case the recording gets stuck for any reason
+ var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1)));
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ }
+ else
+ {
+ // The media source if infinite so we need to handle stopping ourselves
+ var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMilliseconds(bufferMs)));
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ }
+
+ var tempFileTask = response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken);
+
+ // Give the temp file a little time to build up
+ await Task.Delay(bufferMs, cancellationToken).ConfigureAwait(false);
+
+ var recordTask = Task.Run(() => RecordFromFile(mediaSource, tempFile, targetFile, duration, onStarted, cancellationToken), cancellationToken);
+
+ await tempFileTask.ConfigureAwait(false);
+
+ await recordTask.ConfigureAwait(false);
+ }
+ }
+
+ _logger.Info("Recording completed to file {0}", targetFile);
+ }
+
+ private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
+ {
_targetPath = targetFile;
_fileSystem.CreateDirectory(Path.GetDirectoryName(targetFile));
@@ -52,12 +139,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
UseShellExecute = false,
// Must consume both stdout and stderr or deadlocks may occur
- RedirectStandardOutput = true,
+ //RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = _mediaEncoder.EncoderPath,
- Arguments = GetCommandLineArgs(mediaSource, targetFile, duration),
+ Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration),
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
@@ -78,7 +165,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_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);
+ _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
process.Exited += (sender, args) => OnFfMpegProcessExited(process);
@@ -87,24 +174,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
cancellationToken.Register(Stop);
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
- process.BeginOutputReadLine();
+ //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);
- await _taskCompletionSource.Task.ConfigureAwait(false);
+ return _taskCompletionSource.Task;
}
- private string GetCommandLineArgs(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration)
+ private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile, TimeSpan duration)
{
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",
+ "-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 -1 -profile:v high -level 41",
GetOutputSizeParam(),
maxBitrate.ToString(CultureInfo.InvariantCulture));
}
@@ -113,23 +200,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
videoArgs = "-codec:v:0 copy";
}
- var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -i \"{0}\" -t {4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
+ var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
+ var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -re -i \"{0}\"{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), _mediaEncoder.GetTimeParameter(duration.Ticks));
+ commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), durationParam);
return commandLineArgs;
}
private string GetAudioArgs(MediaSourceInfo mediaSource)
{
- var copyAudio = new[] { "aac", "mp3" };
+ // do not copy aac because many players have difficulty with aac_latm
+ var copyAudio = new[] { "mp3" };
var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>();
- if (mediaStreams.Any(i => i.Type == MediaStreamType.Audio && copyAudio.Contains(i.Codec, StringComparer.OrdinalIgnoreCase)))
+ var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty;
+
+ if (copyAudio.Contains(inputAudioCodec, StringComparer.OrdinalIgnoreCase))
+ {
+ return "-codec:a:0 copy";
+ }
+ if (_liveTvOptions.EnableOriginalAudioWithEncodedRecordings && !string.Equals(inputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
{
return "-codec:a:0 copy";
}
@@ -175,9 +270,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
//process.Kill();
_process.StandardInput.WriteLine("q");
-
- // Need to wait because killing is asynchronous
- _process.WaitForExit(5000);
}
catch (Exception ex)
{
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
index 268a4f751..5706b6ae9 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
@@ -17,5 +17,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
+
+ string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 5d462f106..423358906 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Threading;
using CommonIO;
using MediaBrowser.Controller.Power;
+using MediaBrowser.Model.LiveTv;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
@@ -71,6 +72,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
+ public void AddOrUpdate(TimerInfo item, bool resetTimer)
+ {
+ if (resetTimer)
+ {
+ AddOrUpdate(item);
+ return;
+ }
+
+ var list = GetAll().ToList();
+
+ if (!list.Any(i => EqualityComparer(i, item)))
+ {
+ base.Add(item);
+ }
+ else
+ {
+ base.Update(item);
+ }
+ }
+
public override void Add(TimerInfo item)
{
if (string.IsNullOrWhiteSpace(item.Id))
@@ -85,6 +106,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
private void AddTimer(TimerInfo item)
{
+ if (item.Status == RecordingStatus.Completed)
+ {
+ return;
+ }
+
var startDate = RecordingHelper.GetStartTime(item);
var now = DateTime.UtcNow;
@@ -117,15 +143,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- public void StartTimer(TimerInfo item, TimeSpan length)
+ public void StartTimer(TimerInfo item, TimeSpan dueTime)
{
StopTimer(item);
- var timer = new Timer(TimerCallback, item.Id, length, TimeSpan.Zero);
+ var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero);
if (_timers.TryAdd(item.Id, timer))
{
- _logger.Info("Creating recording timer for {0}, {1}. Timer will fire in {2} minutes", item.Id, item.Name, length.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+ _logger.Info("Creating recording timer for {0}, {1}. Timer will fire in {2} minutes", item.Id, item.Name, dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
}
else
{
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index ae2a85090..e37109c14 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -506,8 +506,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
}
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
- ListingsProviderInfo info,
- List<string> programIds,
+ ListingsProviderInfo info,
+ List<string> programIds,
CancellationToken cancellationToken)
{
var imageIdString = "[";
@@ -564,7 +564,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
try
{
- using (Stream responce = await Get(options, false, info).ConfigureAwait(false))
+ using (Stream responce = await Get(options, false, info).ConfigureAwait(false))
{
var root = _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.Headends>>(responce);
@@ -666,58 +666,60 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
}
}
- private async Task<HttpResponseInfo> Post(HttpRequestOptions options,
- bool enableRetry,
- ListingsProviderInfo providerInfo)
+ private async Task<HttpResponseInfo> Post(HttpRequestOptions options,
+ bool enableRetry,
+ ListingsProviderInfo providerInfo)
{
try
{
return await _httpClient.Post(options).ConfigureAwait(false);
- }
- catch (HttpException ex)
- {
- _tokens.Clear();
-
- if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
- {
- enableRetry = false;
- }
-
- if (!enableRetry) {
- throw;
- }
- }
-
- var newToken = await GetToken (providerInfo, options.CancellationToken).ConfigureAwait (false);
- options.RequestHeaders ["token"] = newToken;
- return await Post (options, false, providerInfo).ConfigureAwait (false);
+ }
+ catch (HttpException ex)
+ {
+ _tokens.Clear();
+
+ if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
+ {
+ enableRetry = false;
+ }
+
+ if (!enableRetry)
+ {
+ throw;
+ }
+ }
+
+ var newToken = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
+ options.RequestHeaders["token"] = newToken;
+ return await Post(options, false, providerInfo).ConfigureAwait(false);
}
- private async Task<Stream> Get(HttpRequestOptions options,
- bool enableRetry,
- ListingsProviderInfo providerInfo)
+ private async Task<Stream> Get(HttpRequestOptions options,
+ bool enableRetry,
+ ListingsProviderInfo providerInfo)
{
try
{
return await _httpClient.Get(options).ConfigureAwait(false);
- }
- catch (HttpException ex)
- {
- _tokens.Clear();
-
- if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
- {
- enableRetry = false;
- }
-
- if (!enableRetry) {
- throw;
- }
- }
-
- var newToken = await GetToken (providerInfo, options.CancellationToken).ConfigureAwait (false);
- options.RequestHeaders ["token"] = newToken;
- return await Get (options, false, providerInfo).ConfigureAwait (false);
+ }
+ catch (HttpException ex)
+ {
+ _tokens.Clear();
+
+ if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
+ {
+ enableRetry = false;
+ }
+
+ if (!enableRetry)
+ {
+ throw;
+ }
+ }
+
+ var newToken = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
+ options.RequestHeaders["token"] = newToken;
+ return await Get(options, false, providerInfo).ConfigureAwait(false);
}
private async Task<string> GetTokenInternal(string username, string password,
@@ -734,7 +736,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
//_logger.Info("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " +
// httpOptions.RequestContent);
- using (var responce = await Post(httpOptions, false, null).ConfigureAwait(false))
+ using (var responce = await Post(httpOptions, false, null).ConfigureAwait(false))
{
var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Token>(responce.Content);
if (root.message == "OK")
@@ -816,7 +818,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
try
{
- using (var response = await Get(options, false, null).ConfigureAwait(false))
+ using (var response = await Get(options, false, null).ConfigureAwait(false))
{
var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Lineups>(response);
@@ -869,6 +871,75 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
return GetHeadends(info, country, location, CancellationToken.None);
}
+ public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken)
+ {
+ var listingsId = info.ListingsId;
+ if (string.IsNullOrWhiteSpace(listingsId))
+ {
+ throw new Exception("ListingsId required");
+ }
+
+ await AddMetadata(info, new List<ChannelInfo>(), cancellationToken).ConfigureAwait(false);
+
+ var token = await GetToken(info, cancellationToken);
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ throw new Exception("token required");
+ }
+
+ var httpOptions = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/lineups/" + listingsId,
+ UserAgent = UserAgent,
+ CancellationToken = cancellationToken,
+ LogErrorResponseBody = true,
+ // The data can be large so give it some extra time
+ TimeoutMs = 60000
+ };
+
+ httpOptions.RequestHeaders["token"] = token;
+
+ var list = new List<ChannelInfo>();
+
+ using (var response = await Get(httpOptions, true, info).ConfigureAwait(false))
+ {
+ var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Channel>(response);
+ _logger.Info("Found " + root.map.Count + " channels on the lineup on ScheduleDirect");
+ _logger.Info("Mapping Stations to Channel");
+ foreach (ScheduleDirect.Map map in root.map)
+ {
+ var channelNumber = map.logicalChannelNumber;
+
+ if (string.IsNullOrWhiteSpace(channelNumber))
+ {
+ channelNumber = map.channel;
+ }
+ if (string.IsNullOrWhiteSpace(channelNumber))
+ {
+ channelNumber = map.atscMajor + "." + map.atscMinor;
+ }
+ channelNumber = channelNumber.TrimStart('0');
+
+ var name = channelNumber;
+ var station = GetStation(listingsId, channelNumber, null);
+
+ if (station != null)
+ {
+ name = station.name;
+ }
+
+ list.Add(new ChannelInfo
+ {
+ Number = channelNumber,
+ Name = name
+ });
+ }
+ }
+
+ return list;
+ }
+
public class ScheduleDirect
{
public class Token
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs
deleted file mode 100644
index ac316f9a1..000000000
--- a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.LiveTv;
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Server.Implementations.LiveTv.Listings
-{
- public class XmlTv : IListingsProvider
- {
- public string Name
- {
- get { return "XmlTV"; }
- }
-
- public string Type
- {
- get { return "xmltv"; }
- }
-
- public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
-
- public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken)
- {
- // Might not be needed
- }
-
- public async Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
- {
- // Check that the path or url is valid. If not, throw a file not found exception
- }
-
- public Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
- {
- // In theory this should never be called because there is always only one lineup
- throw new NotImplementedException();
- }
- }
-}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
new file mode 100644
index 000000000..1e82e3fce
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -0,0 +1,219 @@
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.XmlTv.Classes;
+using Emby.XmlTv.Entities;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.Listings
+{
+ public class XmlTvListingsProvider : IListingsProvider
+ {
+ private readonly IServerConfigurationManager _config;
+ private readonly IHttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger)
+ {
+ _config = config;
+ _httpClient = httpClient;
+ _logger = logger;
+ }
+
+ public string Name
+ {
+ get { return "XmlTV"; }
+ }
+
+ public string Type
+ {
+ get { return "xmltv"; }
+ }
+
+ private string GetLanguage()
+ {
+ return _config.Configuration.PreferredMetadataLanguage;
+ }
+
+ private async Task<string> GetXml(string path, CancellationToken cancellationToken)
+ {
+ _logger.Info("xmltv path: {0}", path);
+
+ if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ return path;
+ }
+
+ var cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml";
+ var cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
+ if (File.Exists(cacheFile))
+ {
+ return cacheFile;
+ }
+
+ _logger.Info("Downloading xmltv listings from {0}", path);
+
+ var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = path,
+ Progress = new Progress<Double>(),
+ DecompressionMethod = DecompressionMethods.GZip,
+
+ // It's going to come back gzipped regardless of this value
+ // So we need to make sure the decompression method is set to gzip
+ EnableHttpCompression = true
+
+ }).ConfigureAwait(false);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
+
+ using (var stream = File.OpenRead(tempFile))
+ {
+ using (var reader = new StreamReader(stream, Encoding.UTF8))
+ {
+ using (var fileStream = File.OpenWrite(cacheFile))
+ {
+ using (var writer = new StreamWriter(fileStream))
+ {
+ while (!reader.EndOfStream)
+ {
+ writer.WriteLine(reader.ReadLine());
+ }
+ }
+ }
+ }
+ }
+
+ _logger.Debug("Returning xmltv path {0}", cacheFile);
+ return cacheFile;
+ }
+
+ public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+ {
+ if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false))
+ {
+ var length = endDateUtc - startDateUtc;
+ if (length.TotalDays > 1)
+ {
+ endDateUtc = startDateUtc.AddDays(1);
+ }
+ }
+
+ var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
+ var reader = new XmlTvReader(path, GetLanguage(), null);
+
+ var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken);
+ return results.Select(p => new ProgramInfo()
+ {
+ ChannelId = p.ChannelId,
+ EndDate = GetDate(p.EndDate),
+ EpisodeNumber = p.Episode == null ? null : p.Episode.Episode,
+ EpisodeTitle = p.Episode == null ? null : p.Episode.Title,
+ Genres = p.Categories,
+ Id = String.Format("{0}_{1:O}", p.ChannelId, p.StartDate), // Construct an id from the channel and start date,
+ StartDate = GetDate(p.StartDate),
+ Name = p.Title,
+ Overview = p.Description,
+ ShortOverview = p.Description,
+ ProductionYear = !p.CopyrightDate.HasValue ? (int?)null : p.CopyrightDate.Value.Year,
+ SeasonNumber = p.Episode == null ? null : p.Episode.Series,
+ IsSeries = p.Episode != null,
+ IsRepeat = p.IsRepeat,
+ IsPremiere = p.Premiere != null,
+ IsKids = p.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)),
+ IsMovie = p.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)),
+ IsNews = p.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)),
+ IsSports = p.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)),
+ ImageUrl = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source) ? p.Icon.Source : null,
+ HasImage = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source),
+ OfficialRating = p.Rating != null && !String.IsNullOrEmpty(p.Rating.Value) ? p.Rating.Value : null,
+ CommunityRating = p.StarRating.HasValue ? p.StarRating.Value : (float?)null,
+ SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null
+ });
+ }
+
+ private DateTime GetDate(DateTime date)
+ {
+ if (date.Kind != DateTimeKind.Utc)
+ {
+ date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
+ }
+ return date;
+ }
+
+ public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken)
+ {
+ // Add the channel image url
+ var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
+ var reader = new XmlTvReader(path, GetLanguage(), null);
+ var results = reader.GetChannels().ToList();
+
+ if (channels != null)
+ {
+ channels.ForEach(c =>
+ {
+ var channelNumber = info.GetMappedChannel(c.Number);
+ var match = results.FirstOrDefault(r => string.Equals(r.Id, channelNumber, StringComparison.OrdinalIgnoreCase));
+
+ if (match != null && match.Icon != null && !String.IsNullOrEmpty(match.Icon.Source))
+ {
+ c.ImageUrl = match.Icon.Source;
+ }
+ });
+ }
+ }
+
+ public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
+ {
+ // Assume all urls are valid. check files for existence
+ if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !File.Exists(info.Path))
+ {
+ throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path);
+ }
+
+ return Task.FromResult(true);
+ }
+
+ public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
+ {
+ // In theory this should never be called because there is always only one lineup
+ var path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false);
+ var reader = new XmlTvReader(path, GetLanguage(), null);
+ var results = reader.GetChannels();
+
+ // Should this method be async?
+ return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList();
+ }
+
+ public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken)
+ {
+ // In theory this should never be called because there is always only one lineup
+ var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
+ var reader = new XmlTvReader(path, GetLanguage(), null);
+ var results = reader.GetChannels();
+
+ // Should this method be async?
+ return results.Select(c => new ChannelInfo()
+ {
+ Id = c.Id,
+ Name = c.DisplayName,
+ ImageUrl = c.Icon != null && !String.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null,
+ Number = c.Id
+
+ }).ToList();
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
index 3849f44ab..64af35a9a 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -30,6 +30,8 @@ using System.Threading.Tasks;
using CommonIO;
using IniParser;
using IniParser.Model;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Model.Events;
namespace MediaBrowser.Server.Implementations.LiveTv
{
@@ -64,6 +66,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private readonly List<IListingsProvider> _listingProviders = new List<IListingsProvider>();
private readonly IFileSystem _fileSystem;
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
+ public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
+
public LiveTvManager(IApplicationHost appHost, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager, IFileSystem fileSystem)
{
_config = config;
@@ -133,9 +140,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId);
+ var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false);
+
var channels = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }
+ IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
+ SortBy = new[] { ItemSortBy.SortName },
+ TopParentIds = new[] { topFolder.Id.ToString("N") }
}).Cast<LiveTvChannel>();
@@ -164,7 +175,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var val = query.IsFavorite.Value;
channels = channels
- .Where(i => _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite == val);
+ .Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val);
}
if (query.IsLiked.HasValue)
@@ -174,7 +185,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
channels = channels
.Where(i =>
{
- var likes = _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).Likes;
+ var likes = _userDataManager.GetUserData(user, i).Likes;
return likes.HasValue && likes.Value == val;
});
@@ -187,7 +198,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
channels = channels
.Where(i =>
{
- var likes = _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).Likes;
+ var likes = _userDataManager.GetUserData(user, i).Likes;
return likes.HasValue && likes.Value != val;
});
@@ -200,7 +211,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
if (enableFavoriteSorting)
{
- var userData = _userDataManager.GetUserData(user.Id, i.GetUserDataKey());
+ var userData = _userDataManager.GetUserData(user, i);
if (userData.IsFavorite)
{
@@ -511,14 +522,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv
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;
+ mediaSource.SupportsDirectStream = false;
mediaSource.SupportsTranscoding = true;
+ foreach (var stream in mediaSource.MediaStreams)
+ {
+ if (stream.Type == MediaStreamType.Video && string.IsNullOrWhiteSpace(stream.NalLengthSize))
+ {
+ stream.NalLengthSize = "0";
+ }
+ }
}
}
private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, Guid parentFolderId, CancellationToken cancellationToken)
{
var isNew = false;
+ var forceUpdate = false;
var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id);
@@ -568,10 +587,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath))
{
item.SetImagePath(ImageType.Primary, channelInfo.ImagePath);
+ forceUpdate = true;
}
else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl))
{
item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl);
+ forceUpdate = true;
}
}
@@ -580,9 +601,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv
item.Name = channelInfo.Name;
}
+ if (isNew)
+ {
+ await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
+ }
+ else if (forceUpdate)
+ {
+ await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ }
+
await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem)
{
- ForceSave = isNew
+ ForceSave = isNew || forceUpdate
}, cancellationToken);
@@ -626,7 +656,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
item.Audio = info.Audio;
item.ChannelId = channel.Id.ToString("N");
item.CommunityRating = item.CommunityRating ?? info.CommunityRating;
- item.EndDate = info.EndDate;
+
item.EpisodeTitle = info.EpisodeTitle;
item.ExternalId = info.Id;
item.Genres = info.Genres;
@@ -643,7 +673,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv
item.OfficialRating = item.OfficialRating ?? info.OfficialRating;
item.Overview = item.Overview ?? info.Overview;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
+
+ if (item.StartDate != info.StartDate)
+ {
+ forceUpdate = true;
+ }
item.StartDate = info.StartDate;
+
+ if (item.EndDate != info.EndDate)
+ {
+ forceUpdate = true;
+ }
+ item.EndDate = info.EndDate;
+
item.HomePageUrl = info.HomePageUrl;
item.ProductionYear = info.ProductionYear;
@@ -864,6 +906,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId);
+ var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false);
+
+ if (query.SortBy.Length == 0)
+ {
+ // Unless something else was specified, order by start date to take advantage of a specialized index
+ query.SortBy = new[] { ItemSortBy.StartDate };
+ }
+
var internalQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
@@ -879,7 +929,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
StartIndex = query.StartIndex,
Limit = query.Limit,
SortBy = query.SortBy,
- SortOrder = query.SortOrder ?? SortOrder.Ascending
+ SortOrder = query.SortOrder ?? SortOrder.Ascending,
+ EnableTotalRecordCount = query.EnableTotalRecordCount,
+ TopParentIds = new[] { topFolder.Id.ToString("N") }
};
if (query.HasAired.HasValue)
@@ -896,7 +948,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var queryResult = _libraryManager.QueryItems(internalQuery);
- var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user).ToArray();
+ var returnArray = (await _dtoService.GetBaseItemDtos(queryResult.Items, options, user).ConfigureAwait(false)).ToArray();
var result = new QueryResult<BaseItemDto>
{
@@ -911,15 +963,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
var user = _userManager.GetUserById(query.UserId);
+ var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false);
+
var internalQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
IsAiring = query.IsAiring,
IsMovie = query.IsMovie,
IsSports = query.IsSports,
- IsKids = query.IsKids
+ IsKids = query.IsKids,
+ EnableTotalRecordCount = query.EnableTotalRecordCount,
+ SortBy = new[] { ItemSortBy.StartDate },
+ TopParentIds = new[] { topFolder.Id.ToString("N") }
};
+ if (query.Limit.HasValue)
+ {
+ internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200);
+ }
+
if (query.HasAired.HasValue)
{
if (query.HasAired.Value)
@@ -936,15 +998,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var programList = programs.ToList();
- var genres = programList.SelectMany(i => i.Genres)
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .DistinctNames()
- .Select(i => _libraryManager.GetGenre(i))
- .DistinctBy(i => i.Id)
- .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
+ var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false);
programs = programList.OrderBy(i => i.HasImage(ImageType.Primary) ? 0 : 1)
- .ThenByDescending(i => GetRecommendationScore(i, user.Id, genres))
+ .ThenByDescending(i => GetRecommendationScore(i, user.Id, factorChannelWatchCount))
.ThenBy(i => i.StartDate);
if (query.Limit.HasValue)
@@ -971,7 +1028,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var user = _userManager.GetUserById(query.UserId);
- var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ToArray();
+ var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray();
var result = new QueryResult<BaseItemDto>
{
@@ -982,7 +1039,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return result;
}
- private int GetRecommendationScore(LiveTvProgram program, Guid userId, Dictionary<string, Genre> genres)
+ private int GetRecommendationScore(LiveTvProgram program, Guid userId, bool factorChannelWatchCount)
{
var score = 0;
@@ -998,7 +1055,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var channel = GetInternalChannel(program.ChannelId);
- var channelUserdata = _userDataManager.GetUserData(userId, channel.GetUserDataKey());
+ var channelUserdata = _userDataManager.GetUserData(userId, channel);
if (channelUserdata.Likes ?? false)
{
@@ -1014,41 +1071,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
score += 3;
}
- score += GetGenreScore(program.Genres, userId, genres);
-
- return score;
- }
-
- private int GetGenreScore(IEnumerable<string> programGenres, Guid userId, Dictionary<string, Genre> genres)
- {
- return programGenres.Select(i =>
+ if (factorChannelWatchCount)
{
- var score = 0;
-
- Genre genre;
-
- if (genres.TryGetValue(i, out genre))
- {
- var genreUserdata = _userDataManager.GetUserData(userId, genre.GetUserDataKey());
-
- if (genreUserdata.Likes ?? false)
- {
- score++;
- }
- else if (!(genreUserdata.Likes ?? true))
- {
- score--;
- }
-
- if (genreUserdata.IsFavorite)
- {
- score += 2;
- }
- }
-
- return score;
+ score += channelUserdata.PlayCount;
+ }
- }).Sum();
+ return score;
}
private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken)
@@ -1104,6 +1132,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private async Task RefreshChannelsInternal(IProgress<double> progress, CancellationToken cancellationToken)
{
+ EmbyTV.EmbyTV.Current.CreateRecordingFolders();
+
var numComplete = 0;
double progressPerService = _services.Count == 0
? 0
@@ -1184,8 +1214,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolderId, cancellationToken).ConfigureAwait(false);
list.Add(item);
-
- _libraryManager.RegisterItem(item);
}
catch (OperationCanceledException)
{
@@ -1256,11 +1284,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private async Task CleanDatabaseInternal(List<Guid> currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
{
- var list = _itemRepo.GetItemIds(new InternalItemsQuery
+ var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
{
IncludeItemTypes = validTypes
- }).Items.ToList();
+ }).ToList();
var numComplete = 0;
@@ -1373,6 +1401,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
+ private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, User user)
+ {
+ if (user == null || (query.IsInProgress ?? false))
+ {
+ return new QueryResult<BaseItem>();
+ }
+
+ var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
+ .SelectMany(i => i.Locations)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Select(i => _libraryManager.FindByPath(i, true))
+ .Where(i => i != null)
+ .Where(i => i.IsVisibleStandalone(user))
+ .ToList();
+
+ if (folders.Count == 0)
+ {
+ return new QueryResult<BaseItem>();
+ }
+
+ return _libraryManager.GetItemsResult(new InternalItemsQuery(user)
+ {
+ MediaTypes = new[] { MediaType.Video },
+ Recursive = true,
+ AncestorIds = folders.Select(i => i.Id.ToString("N")).ToArray(),
+ IsFolder = false,
+ ExcludeLocationTypes = new[] { LocationType.Virtual },
+ Limit = Math.Min(200, query.Limit ?? int.MaxValue),
+ SortBy = new[] { ItemSortBy.DateCreated },
+ SortOrder = SortOrder.Descending,
+ EnableTotalRecordCount = query.EnableTotalRecordCount
+ });
+ }
+
public async Task<QueryResult<BaseItem>> GetInternalRecordings(RecordingQuery query, CancellationToken cancellationToken)
{
var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId);
@@ -1381,6 +1443,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return new QueryResult<BaseItem>();
}
+ if (_services.Count == 1)
+ {
+ return GetEmbyRecordings(query, user);
+ }
+
await RefreshRecordings(cancellationToken).ConfigureAwait(false);
var internalQuery = new InternalItemsQuery(user)
@@ -1393,7 +1460,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
internalQuery.ChannelIds = new[] { query.ChannelId };
}
- var queryResult = _libraryManager.GetItemList(internalQuery, new string[] { });
+ var queryResult = _libraryManager.GetItemList(internalQuery);
IEnumerable<ILiveTvRecording> recordings = queryResult.Cast<ILiveTvRecording>();
if (!string.IsNullOrWhiteSpace(query.Id))
@@ -1506,6 +1573,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
dto.ChannelName = channel.Name;
dto.MediaType = channel.MediaType;
+ dto.ChannelNumber = channel.Number;
if (channel.HasImage(ImageType.Primary))
{
@@ -1596,7 +1664,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var internalResult = await GetInternalRecordings(query, cancellationToken).ConfigureAwait(false);
- var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ToArray();
+ var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray();
return new QueryResult<BaseItemDto>
{
@@ -1623,6 +1691,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
var timers = results.SelectMany(i => i.ToList());
+ if (query.IsActive.HasValue)
+ {
+ if (query.IsActive.Value)
+ {
+ timers = timers.Where(i => i.Item1.Status == RecordingStatus.InProgress);
+ }
+ else
+ {
+ timers = timers.Where(i => i.Item1.Status != RecordingStatus.InProgress);
+ }
+ }
+
if (!string.IsNullOrEmpty(query.ChannelId))
{
var guid = new Guid(query.ChannelId);
@@ -1724,6 +1804,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
_lastRecordingRefreshTime = DateTime.MinValue;
+
+ EventHelper.QueueEventIfNotNull(TimerCancelled, this, new GenericEventArgs<TimerEventInfo>
+ {
+ Argument = new TimerEventInfo
+ {
+ Id = id
+ }
+ }, _logger);
}
public async Task CancelSeriesTimer(string id)
@@ -1739,6 +1827,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
_lastRecordingRefreshTime = DateTime.MinValue;
+
+ EventHelper.QueueEventIfNotNull(SeriesTimerCancelled, this, new GenericEventArgs<TimerEventInfo>
+ {
+ Argument = new TimerEventInfo
+ {
+ Id = id
+ }
+ }, _logger);
}
public async Task<BaseItemDto> GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null)
@@ -1835,9 +1931,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
MaxStartDate = now,
MinEndDate = now,
Limit = channelIds.Length,
- SortBy = new[] { "StartDate" }
+ SortBy = new[] { "StartDate" },
+ TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Result.Id.ToString("N") }
- }, new string[] { }).ToList();
+ }).ToList();
foreach (var tuple in tuples)
{
@@ -1845,6 +1942,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var channel = tuple.Item2;
dto.Number = channel.Number;
+ dto.ChannelNumber = channel.Number;
dto.ChannelType = channel.ChannelType;
dto.ServiceName = GetService(channel).Name;
@@ -1923,10 +2021,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false);
var info = _tvDtoService.GetSeriesTimerInfoDto(defaults.Item1, defaults.Item2, null);
- info.Days = new List<DayOfWeek>
- {
- program.StartDate.ToLocalTime().DayOfWeek
- };
+ info.Days = defaults.Item1.Days;
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
@@ -1957,9 +2052,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false);
info.Priority = defaultValues.Priority;
- await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false);
+ string newTimerId = null;
+ var supportsNewTimerIds = service as ISupportsNewTimerIds;
+ if (supportsNewTimerIds != null)
+ {
+ newTimerId = await supportsNewTimerIds.CreateTimer(info, cancellationToken).ConfigureAwait(false);
+ newTimerId = _tvDtoService.GetInternalTimerId(timer.ServiceName, newTimerId).ToString("N");
+ }
+ else
+ {
+ await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false);
+ }
+
_lastRecordingRefreshTime = DateTime.MinValue;
_logger.Info("New recording scheduled");
+
+ EventHelper.QueueEventIfNotNull(TimerCreated, this, new GenericEventArgs<TimerEventInfo>
+ {
+ Argument = new TimerEventInfo
+ {
+ ProgramId = _tvDtoService.GetInternalProgramId(timer.ServiceName, info.ProgramId).ToString("N"),
+ Id = newTimerId
+ }
+ }, _logger);
}
public async Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken)
@@ -1972,8 +2087,28 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false);
info.Priority = defaultValues.Priority;
- await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false);
+ string newTimerId = null;
+ var supportsNewTimerIds = service as ISupportsNewTimerIds;
+ if (supportsNewTimerIds != null)
+ {
+ newTimerId = await supportsNewTimerIds.CreateSeriesTimer(info, cancellationToken).ConfigureAwait(false);
+ newTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.ServiceName, newTimerId).ToString("N");
+ }
+ else
+ {
+ await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false);
+ }
+
_lastRecordingRefreshTime = DateTime.MinValue;
+
+ EventHelper.QueueEventIfNotNull(SeriesTimerCreated, this, new GenericEventArgs<TimerEventInfo>
+ {
+ Argument = new TimerEventInfo
+ {
+ ProgramId = _tvDtoService.GetInternalProgramId(timer.ServiceName, info.ProgramId).ToString("N"),
+ Id = newTimerId
+ }
+ }, _logger);
}
public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken)
@@ -2048,7 +2183,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}, cancellationToken).ConfigureAwait(false);
- var recordings = recordingResult.Items.Cast<ILiveTvRecording>().ToList();
+ var recordings = recordingResult.Items.OfType<ILiveTvRecording>().ToList();
var groups = new List<BaseItemDto>();
@@ -2389,6 +2524,79 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return info;
}
+ public void DeleteListingsProvider(string id)
+ {
+ var config = GetConfiguration();
+
+ config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToList();
+
+ _config.SaveConfiguration("livetv", config);
+ _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ }
+
+ public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
+ {
+ var config = GetConfiguration();
+
+ var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
+ listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
+
+ if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
+ {
+ var list = listingsProviderInfo.ChannelMappings.ToList();
+ list.Add(new NameValuePair
+ {
+ Name = tunerChannelNumber,
+ Value = providerChannelNumber
+ });
+ listingsProviderInfo.ChannelMappings = list.ToArray();
+ }
+
+ _config.SaveConfiguration("livetv", config);
+
+ var tunerChannels = await GetChannelsForListingsProvider(providerId, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var providerChannels = await GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var mappings = listingsProviderInfo.ChannelMappings.ToList();
+
+ var tunerChannelMappings =
+ tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
+
+ _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+
+ return tunerChannelMappings.First(i => string.Equals(i.Number, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
+ }
+
+ public TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, List<NameValuePair> mappings, List<ChannelInfo> providerChannels)
+ {
+ var result = new TunerChannelMapping
+ {
+ Name = channel.Number + " " + channel.Name,
+ Number = channel.Number
+ };
+
+ var mapping = mappings.FirstOrDefault(i => string.Equals(i.Name, channel.Number, StringComparison.OrdinalIgnoreCase));
+ var providerChannelNumber = channel.Number;
+
+ if (mapping != null)
+ {
+ providerChannelNumber = mapping.Value;
+ }
+
+ var providerChannel = providerChannels.FirstOrDefault(i => string.Equals(i.Number, providerChannelNumber, StringComparison.OrdinalIgnoreCase));
+
+ if (providerChannel != null)
+ {
+ result.ProviderChannelNumber = providerChannel.Number;
+ result.ProviderChannelName = providerChannel.Name;
+ }
+
+ return result;
+ }
+
public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location)
{
var config = GetConfiguration();
@@ -2450,7 +2658,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
public List<NameValuePair> GetSatIniMappings()
{
- var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini.satellite", StringComparison.OrdinalIgnoreCase) != -1).ToList();
+ var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini", StringComparison.OrdinalIgnoreCase) != -1).ToList();
return names.Select(GetSatIniMappings).Where(i => i != null).DistinctBy(i => i.Value.Split('|')[0]).ToList();
}
@@ -2472,13 +2680,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return null;
}
+ var srch = "SatIp.ini.";
+ var filename = Path.GetFileName(resource);
+
return new NameValuePair
{
Name = satType1 + " " + satType2,
- Value = satType2 + "|" + Path.GetFileName(resource)
+ Value = satType2 + "|" + filename.Substring(filename.IndexOf(srch) + srch.Length)
};
}
}
}
+
+ public Task<List<ChannelInfo>> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken)
+ {
+ return new TunerHosts.SatIp.ChannelScan(_logger).Scan(info, cancellationToken);
+ }
+
+ public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken)
+ {
+ var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
+ return EmbyTV.EmbyTV.Current.GetChannelsForListingsProvider(info, cancellationToken);
+ }
+
+ public Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken)
+ {
+ var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
+ var provider = _listingProviders.First(i => string.Equals(i.Type, info.Type, StringComparison.OrdinalIgnoreCase));
+ return provider.GetChannels(info, cancellationToken);
+ }
}
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index d3bb87bc7..cdba1873e 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -83,7 +83,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
var list = sources.ToList();
- var serverUrl = _appHost.LocalApiUrl;
+ var serverUrl = await _appHost.GetLocalApiUrl().ConfigureAwait(false);
foreach (var source in list)
{
diff --git a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs
index ab8ec720b..3f0538bd0 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs
@@ -74,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
- public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService)
+ public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
{
var liveTvItem = item as LiveTvProgram;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs
index fce3223ea..25678c29d 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs
@@ -68,7 +68,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
get { return 0; }
}
- public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService)
+ public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
{
var liveTvItem = item as ILiveTvRecording;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 02a8d6938..9bb5b4fd7 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -224,7 +224,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
- //await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
+ if (EnableMediaProbing)
+ {
+ await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
+ }
+
return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool);
}
catch (Exception ex)
@@ -239,6 +243,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
throw new LiveTvConflictException();
}
+ protected virtual bool EnableMediaProbing
+ {
+ get { return false; }
+ }
+
protected async Task<bool> IsAvailable(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
{
try
@@ -268,6 +277,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1));
}
+ private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false);
+
+ // Leave the resource locked. it will be released upstream
+ }
+ catch (Exception)
+ {
+ // Release the resource if there's some kind of failure.
+ resourcePool.Release();
+
+ throw;
+ }
+ }
+
private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
{
var originalRuntime = mediaSource.RunTimeTicks;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index db7f6f86c..69b6fb5a9 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -17,6 +17,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Net;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
@@ -59,7 +60,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return id;
}
- protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
+ public string ApplyDuration(string streamPath, TimeSpan duration)
+ {
+ streamPath += streamPath.IndexOf('?') == -1 ? "?" : "&";
+ streamPath += "duration=" + Convert.ToInt32(duration.TotalSeconds).ToString(CultureInfo.InvariantCulture);
+
+ return streamPath;
+ }
+
+ private async Task<IEnumerable<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
{
var options = new HttpRequestOptions
{
@@ -68,45 +77,61 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
};
using (var stream = await _httpClient.Get(options))
{
- var root = JsonSerializer.DeserializeFromStream<List<Channels>>(stream);
+ var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>();
- if (root != null)
+ if (info.ImportFavoritesOnly)
{
- var result = root.Select(i => new ChannelInfo
- {
- Name = i.GuideName,
- Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
- Id = GetChannelId(info, i),
- IsFavorite = i.Favorite,
- TunerHostId = info.Id
+ lineup = lineup.Where(i => i.Favorite).ToList();
+ }
- });
+ return lineup.Where(i => !i.DRM).ToList();
+ }
+ }
- if (info.ImportFavoritesOnly)
- {
- result = result.Where(i => i.IsFavorite ?? true).ToList();
- }
+ protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
+ {
+ var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
- return result;
- }
- return new List<ChannelInfo>();
- }
+ return lineup.Select(i => new ChannelInfo
+ {
+ Name = i.GuideName,
+ Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
+ Id = GetChannelId(info, i),
+ IsFavorite = i.Favorite,
+ TunerHostId = info.Id,
+ IsHD = i.HD == 1,
+ AudioCodec = i.AudioCodec,
+ VideoCodec = i.VideoCodec
+ });
}
private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken)
{
- using (var stream = await _httpClient.Get(new HttpRequestOptions()
+ try
{
- 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 stream = await _httpClient.Get(new HttpRequestOptions()
+ {
+ 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)
+ }))
+ {
+ var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+
+ return response.ModelNumber;
+ }
+ }
+ catch (HttpException ex)
{
- var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
+ {
+ // HDHR4 doesn't have this api
+ return "HDHR";
+ }
- return response.ModelNumber;
+ throw;
}
}
@@ -226,19 +251,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public string GuideNumber { get; set; }
public string GuideName { get; set; }
+ public string VideoCodec { get; set; }
+ public string AudioCodec { get; set; }
public string URL { get; set; }
public bool Favorite { get; set; }
public bool DRM { get; set; }
+ public int HD { get; set; }
}
- private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, string profile)
+ private async Task<MediaSourceInfo> GetMediaSource(TunerHostInfo info, string channelId, string profile)
{
int? width = null;
int? height = null;
bool isInterlaced = true;
- var videoCodec = !string.IsNullOrWhiteSpace(GetEncodingOptions().HardwareAccelerationType) ? null : "mpeg2video";
+ string videoCodec = null;
+ string audioCodec = "ac3";
int? videoBitrate = null;
+ int? audioBitrate = null;
if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase))
{
@@ -256,18 +286,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
videoCodec = "h264";
videoBitrate = 15000000;
}
- else if (string.Equals(profile, "internet720", StringComparison.OrdinalIgnoreCase))
- {
- width = 1280;
- height = 720;
- isInterlaced = false;
- videoCodec = "h264";
- videoBitrate = 8000000;
- }
else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase))
{
- width = 1280;
- height = 720;
+ width = 960;
+ height = 546;
isInterlaced = false;
videoCodec = "h264";
videoBitrate = 2500000;
@@ -297,6 +319,32 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
videoBitrate = 1000000;
}
+ if (string.IsNullOrWhiteSpace(videoCodec))
+ {
+ var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
+ var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase));
+ if (channel != null)
+ {
+ videoCodec = channel.VideoCodec;
+ audioCodec = channel.AudioCodec;
+
+ videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000;
+ audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000;
+ }
+ }
+
+ // normalize
+ if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
+ {
+ videoCodec = "mpeg2video";
+ }
+
+ string nal = null;
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ nal = "0";
+ }
+
var url = GetApiUrl(info, true) + "/auto/v" + channelId;
if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
@@ -319,16 +367,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Codec = videoCodec,
Width = width,
Height = height,
- BitRate = videoBitrate
-
+ BitRate = videoBitrate,
+ NalLengthSize = nal
+
},
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,
- Codec = "ac3",
- BitRate = 192000
+ Codec = audioCodec,
+ BitRate = audioBitrate
}
},
RequiresOpening = false,
@@ -337,7 +386,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Container = "ts",
Id = profile,
SupportsDirectPlay = true,
- SupportsDirectStream = true,
+ SupportsDirectStream = false,
SupportsTranscoding = true
};
@@ -364,21 +413,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
var hdhrId = GetHdHrIdFromChannelId(channelId);
- list.Add(GetMediaSource(info, hdhrId, "native"));
+ list.Add(await GetMediaSource(info, hdhrId, "native").ConfigureAwait(false));
try
{
string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
model = model ?? string.Empty;
- if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
+ if (info.AllowHWTranscoding && (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
{
- list.Insert(0, GetMediaSource(info, hdhrId, "heavy"));
+ list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false));
- list.Add(GetMediaSource(info, hdhrId, "internet480"));
- list.Add(GetMediaSource(info, hdhrId, "internet360"));
- list.Add(GetMediaSource(info, hdhrId, "internet240"));
- list.Add(GetMediaSource(info, hdhrId, "mobile"));
+ list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false));
+ list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false));
+ list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false));
+ list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false));
+ list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false));
}
}
catch (Exception ex)
@@ -409,7 +459,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
var hdhrId = GetHdHrIdFromChannelId(channelId);
- return GetMediaSource(info, hdhrId, streamId);
+ return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false);
}
public async Task Validate(TunerHostInfo info)
@@ -419,16 +469,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return;
}
- // Test it by pulling down the lineup
- using (var stream = await _httpClient.Get(new HttpRequestOptions
+ try
{
- Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
- CancellationToken = CancellationToken.None
- }))
+ // Test it by pulling down the lineup
+ using (var stream = await _httpClient.Get(new HttpRequestOptions
+ {
+ Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
+ CancellationToken = CancellationToken.None
+ }))
+ {
+ var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+
+ info.DeviceId = response.DeviceID;
+ }
+ }
+ catch (HttpException ex)
{
- var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
+ {
+ // HDHR4 doesn't have this api
+ return;
+ }
- info.DeviceId = response.DeviceID;
+ throw;
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 523f14dfc..2a974b545 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -134,7 +134,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
}
},
RequiresOpening = false,
- RequiresClosing = false
+ RequiresClosing = false,
+
+ ReadAtNativeFramerate = true
};
return new List<MediaSourceInfo> { mediaSource };
@@ -146,5 +148,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
return Task.FromResult(true);
}
+
+ public string ApplyDuration(string streamPath, TimeSpan duration)
+ {
+ return streamPath;
+ }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/ChannelScan.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/ChannelScan.cs
new file mode 100644
index 000000000..fdeae25b0
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/ChannelScan.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using IniParser;
+using IniParser.Model;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
+{
+ public class ChannelScan
+ {
+ private readonly ILogger _logger;
+
+ public ChannelScan(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public async Task<List<ChannelInfo>> Scan(TunerHostInfo info, CancellationToken cancellationToken)
+ {
+ var ini = info.SourceA.Split('|')[1];
+ var resource = GetType().Assembly.GetManifestResourceNames().FirstOrDefault(i => i.EndsWith(ini, StringComparison.OrdinalIgnoreCase));
+
+ _logger.Info("Opening ini file {0}", resource);
+ var list = new List<ChannelInfo>();
+
+ using (var stream = GetType().Assembly.GetManifestResourceStream(resource))
+ {
+ using (var reader = new StreamReader(stream))
+ {
+ var parser = new StreamIniDataParser();
+ var data = parser.ReadData(reader);
+
+ var count = GetInt(data, "DVB", "0", 0);
+
+ _logger.Info("DVB Count: {0}", count);
+
+ var index = 1;
+ var source = "1";
+
+ while (index <= count)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (var rtspSession = new RtspSession(info.Url, _logger))
+ {
+ float percent = count == 0 ? 0 : (float)(index) / count;
+ percent = Math.Max(percent * 100, 100);
+
+ //SetControlPropertyThreadSafe(pgbSearchResult, "Value", (int)percent);
+ var strArray = data["DVB"][index.ToString(CultureInfo.InvariantCulture)].Split(',');
+
+ string tuning;
+ if (strArray[4] == "S2")
+ {
+ tuning = string.Format("src={0}&freq={1}&pol={2}&sr={3}&fec={4}&msys=dvbs2&mtype={5}&plts=on&ro=0.35&pids=0,16,17,18,20", source, strArray[0], strArray[1].ToLower(), strArray[2].ToLower(), strArray[3], strArray[5].ToLower());
+ }
+ else
+ {
+ tuning = string.Format("src={0}&freq={1}&pol={2}&sr={3}&fec={4}&msys=dvbs&mtype={5}&pids=0,16,17,18,20", source, strArray[0], strArray[1].ToLower(), strArray[2], strArray[3], strArray[5].ToLower());
+ }
+
+ rtspSession.Setup(tuning, "unicast");
+
+ rtspSession.Play(string.Empty);
+
+ int signallevel;
+ int signalQuality;
+ rtspSession.Describe(out signallevel, out signalQuality);
+
+ await Task.Delay(500).ConfigureAwait(false);
+ index++;
+ }
+ }
+ }
+ }
+
+ return list;
+ }
+
+ private int GetInt(IniData data, string s1, string s2, int defaultValue)
+ {
+ var value = data[s1][s2];
+ int numericValue;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out numericValue))
+ {
+ return numericValue;
+ }
+
+ return defaultValue;
+ }
+ }
+
+ public class SatChannel
+ {
+ // TODO: Add properties
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs
new file mode 100644
index 000000000..5f286f1db
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs
@@ -0,0 +1,88 @@
+/*
+ Copyright (C) <2007-2016> <Kay Diefenthal>
+
+ SatIp.RtspSample is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ SatIp.RtspSample is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+using System.Collections.Generic;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp
+{
+ /// <summary>
+ /// Standard RTSP request methods.
+ /// </summary>
+ public sealed class RtspMethod
+ {
+ public override int GetHashCode()
+ {
+ return (_name != null ? _name.GetHashCode() : 0);
+ }
+
+ private readonly string _name;
+ private static readonly IDictionary<string, RtspMethod> _values = new Dictionary<string, RtspMethod>();
+
+ public static readonly RtspMethod Describe = new RtspMethod("DESCRIBE");
+ public static readonly RtspMethod Announce = new RtspMethod("ANNOUNCE");
+ public static readonly RtspMethod GetParameter = new RtspMethod("GET_PARAMETER");
+ public static readonly RtspMethod Options = new RtspMethod("OPTIONS");
+ public static readonly RtspMethod Pause = new RtspMethod("PAUSE");
+ public static readonly RtspMethod Play = new RtspMethod("PLAY");
+ public static readonly RtspMethod Record = new RtspMethod("RECORD");
+ public static readonly RtspMethod Redirect = new RtspMethod("REDIRECT");
+ public static readonly RtspMethod Setup = new RtspMethod("SETUP");
+ public static readonly RtspMethod SetParameter = new RtspMethod("SET_PARAMETER");
+ public static readonly RtspMethod Teardown = new RtspMethod("TEARDOWN");
+
+ private RtspMethod(string name)
+ {
+ _name = name;
+ _values.Add(name, this);
+ }
+
+ public override string ToString()
+ {
+ return _name;
+ }
+
+ public override bool Equals(object obj)
+ {
+ var method = obj as RtspMethod;
+ if (method != null && this == method)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ public static ICollection<RtspMethod> Values
+ {
+ get { return _values.Values; }
+ }
+
+ public static explicit operator RtspMethod(string name)
+ {
+ RtspMethod value;
+ if (!_values.TryGetValue(name, out value))
+ {
+ return null;
+ }
+ return value;
+ }
+
+ public static implicit operator string(RtspMethod method)
+ {
+ return method._name;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs
new file mode 100644
index 000000000..600eda02d
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs
@@ -0,0 +1,140 @@
+/*
+ Copyright (C) <2007-2016> <Kay Diefenthal>
+
+ SatIp.RtspSample is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ SatIp.RtspSample is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+using System.Collections.Generic;
+using System.Text;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp
+{
+ /// <summary>
+ /// A simple class that can be used to serialise RTSP requests.
+ /// </summary>
+ public class RtspRequest
+ {
+ private readonly RtspMethod _method;
+ private readonly string _uri;
+ private readonly int _majorVersion;
+ private readonly int _minorVersion;
+ private IDictionary<string, string> _headers = new Dictionary<string, string>();
+ private string _body = string.Empty;
+
+ /// <summary>
+ /// Initialise a new instance of the <see cref="RtspRequest"/> class.
+ /// </summary>
+ /// <param name="method">The request method.</param>
+ /// <param name="uri">The request URI</param>
+ /// <param name="majorVersion">The major version number.</param>
+ /// <param name="minorVersion">The minor version number.</param>
+ public RtspRequest(RtspMethod method, string uri, int majorVersion, int minorVersion)
+ {
+ _method = method;
+ _uri = uri;
+ _majorVersion = majorVersion;
+ _minorVersion = minorVersion;
+ }
+
+ /// <summary>
+ /// Get the request method.
+ /// </summary>
+ public RtspMethod Method
+ {
+ get
+ {
+ return _method;
+ }
+ }
+
+ /// <summary>
+ /// Get the request URI.
+ /// </summary>
+ public string Uri
+ {
+ get
+ {
+ return _uri;
+ }
+ }
+
+ /// <summary>
+ /// Get the request major version number.
+ /// </summary>
+ public int MajorVersion
+ {
+ get
+ {
+ return _majorVersion;
+ }
+ }
+
+ /// <summary>
+ /// Get the request minor version number.
+ /// </summary>
+ public int MinorVersion
+ {
+ get
+ {
+ return _minorVersion;
+ }
+ }
+
+ /// <summary>
+ /// Get or set the request headers.
+ /// </summary>
+ public IDictionary<string, string> Headers
+ {
+ get
+ {
+ return _headers;
+ }
+ set
+ {
+ _headers = value;
+ }
+ }
+
+ /// <summary>
+ /// Get or set the request body.
+ /// </summary>
+ public string Body
+ {
+ get
+ {
+ return _body;
+ }
+ set
+ {
+ _body = value;
+ }
+ }
+
+ /// <summary>
+ /// Serialise this request.
+ /// </summary>
+ /// <returns>raw request bytes</returns>
+ public byte[] Serialise()
+ {
+ var request = new StringBuilder();
+ request.AppendFormat("{0} {1} RTSP/{2}.{3}\r\n", _method, _uri, _majorVersion, _minorVersion);
+ foreach (var header in _headers)
+ {
+ request.AppendFormat("{0}: {1}\r\n", header.Key, header.Value);
+ }
+ request.AppendFormat("\r\n{0}", _body);
+ return Encoding.UTF8.GetBytes(request.ToString());
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs
new file mode 100644
index 000000000..97290623b
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs
@@ -0,0 +1,149 @@
+/*
+ Copyright (C) <2007-2016> <Kay Diefenthal>
+
+ SatIp.RtspSample is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ SatIp.RtspSample is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp
+{
+ /// <summary>
+ /// A simple class that can be used to deserialise RTSP responses.
+ /// </summary>
+ public class RtspResponse
+ {
+ private static readonly Regex RegexStatusLine = new Regex(@"RTSP/(\d+)\.(\d+)\s+(\d+)\s+([^.]+?)\r\n(.*)", RegexOptions.Singleline);
+
+ private int _majorVersion = 1;
+ private int _minorVersion;
+ private RtspStatusCode _statusCode;
+ private string _reasonPhrase;
+ private IDictionary<string, string> _headers;
+ private string _body;
+
+ /// <summary>
+ /// Initialise a new instance of the <see cref="RtspResponse"/> class.
+ /// </summary>
+ private RtspResponse()
+ {
+ }
+
+ /// <summary>
+ /// Get the response major version number.
+ /// </summary>
+ public int MajorVersion
+ {
+ get
+ {
+ return _majorVersion;
+ }
+ }
+
+ /// <summary>
+ /// Get the response minor version number.
+ /// </summary>
+ public int MinorVersion
+ {
+ get
+ {
+ return _minorVersion;
+ }
+ }
+
+ /// <summary>
+ /// Get the response status code.
+ /// </summary>
+ public RtspStatusCode StatusCode
+ {
+ get
+ {
+ return _statusCode;
+ }
+ }
+
+ /// <summary>
+ /// Get the response reason phrase.
+ /// </summary>
+ public string ReasonPhrase
+ {
+ get
+ {
+ return _reasonPhrase;
+ }
+ }
+
+ /// <summary>
+ /// Get the response headers.
+ /// </summary>
+ public IDictionary<string, string> Headers
+ {
+ get
+ {
+ return _headers;
+ }
+ }
+
+ /// <summary>
+ /// Get the response body.
+ /// </summary>
+ public string Body
+ {
+ get
+ {
+ return _body;
+ }
+ set
+ {
+ _body = value;
+ }
+ }
+
+ /// <summary>
+ /// Deserialise/parse an RTSP response.
+ /// </summary>
+ /// <param name="responseBytes">The raw response bytes.</param>
+ /// <param name="responseByteCount">The number of valid bytes in the response.</param>
+ /// <returns>a response object</returns>
+ public static RtspResponse Deserialise(byte[] responseBytes, int responseByteCount)
+ {
+ var response = new RtspResponse();
+ var responseString = Encoding.UTF8.GetString(responseBytes, 0, responseByteCount);
+
+ var m = RegexStatusLine.Match(responseString);
+ if (m.Success)
+ {
+ response._majorVersion = int.Parse(m.Groups[1].Captures[0].Value);
+ response._minorVersion = int.Parse(m.Groups[2].Captures[0].Value);
+ response._statusCode = (RtspStatusCode)int.Parse(m.Groups[3].Captures[0].Value);
+ response._reasonPhrase = m.Groups[4].Captures[0].Value;
+ responseString = m.Groups[5].Captures[0].Value;
+ }
+
+ var sections = responseString.Split(new[] { "\r\n\r\n" }, StringSplitOptions.None);
+ response._body = sections[1];
+ var headers = sections[0].Split(new[] { "\r\n" }, StringSplitOptions.None);
+ response._headers = new Dictionary<string, string>();
+ foreach (var headerInfo in headers.Select(header => header.Split(':')))
+ {
+ response._headers.Add(headerInfo[0], headerInfo[1].Trim());
+ }
+ return response;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs
new file mode 100644
index 000000000..71b3f8a18
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs
@@ -0,0 +1,688 @@
+/*
+ Copyright (C) <2007-2016> <Kay Diefenthal>
+
+ SatIp.RtspSample is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ SatIp.RtspSample is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Text.RegularExpressions;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp
+{
+ public class RtspSession : IDisposable
+ {
+ #region Private Fields
+ private static readonly Regex RegexRtspSessionHeader = new Regex(@"\s*([^\s;]+)(;timeout=(\d+))?");
+ private const int DefaultRtspSessionTimeout = 30; // unit = s
+ private static readonly Regex RegexDescribeResponseSignalInfo = new Regex(@";tuner=\d+,(\d+),(\d+),(\d+),", RegexOptions.Singleline | RegexOptions.IgnoreCase);
+ private string _address;
+ private string _rtspSessionId;
+
+ public string RtspSessionId
+ {
+ get { return _rtspSessionId; }
+ set { _rtspSessionId = value; }
+ }
+ private int _rtspSessionTimeToLive = 0;
+ private string _rtspStreamId;
+ private int _clientRtpPort;
+ private int _clientRtcpPort;
+ private int _serverRtpPort;
+ private int _serverRtcpPort;
+ private int _rtpPort;
+ private int _rtcpPort;
+ private string _rtspStreamUrl;
+ private string _destination;
+ private string _source;
+ private string _transport;
+ private int _signalLevel;
+ private int _signalQuality;
+ private Socket _rtspSocket;
+ private int _rtspSequenceNum = 1;
+ private bool _disposed = false;
+ private readonly ILogger _logger;
+ #endregion
+
+ #region Constructor
+
+ public RtspSession(string address, ILogger logger)
+ {
+ if (string.IsNullOrWhiteSpace(address))
+ {
+ throw new ArgumentNullException("address");
+ }
+
+ _address = address;
+ _logger = logger;
+
+ _logger.Info("Creating RtspSession with url {0}", address);
+ }
+ ~RtspSession()
+ {
+ Dispose(false);
+ }
+ #endregion
+
+ #region Properties
+
+ #region Rtsp
+
+ public string RtspStreamId
+ {
+ get { return _rtspStreamId; }
+ set { if (_rtspStreamId != value) { _rtspStreamId = value; OnPropertyChanged("RtspStreamId"); } }
+ }
+ public string RtspStreamUrl
+ {
+ get { return _rtspStreamUrl; }
+ set { if (_rtspStreamUrl != value) { _rtspStreamUrl = value; OnPropertyChanged("RtspStreamUrl"); } }
+ }
+
+ public int RtspSessionTimeToLive
+ {
+ get
+ {
+ if (_rtspSessionTimeToLive == 0)
+ _rtspSessionTimeToLive = DefaultRtspSessionTimeout;
+ return _rtspSessionTimeToLive * 1000 - 20;
+ }
+ set { if (_rtspSessionTimeToLive != value) { _rtspSessionTimeToLive = value; OnPropertyChanged("RtspSessionTimeToLive"); } }
+ }
+
+ #endregion
+
+ #region Rtp Rtcp
+
+ /// <summary>
+ /// The LocalEndPoint Address
+ /// </summary>
+ public string Destination
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(_destination))
+ {
+ var result = "";
+ var host = Dns.GetHostName();
+ var hostentry = Dns.GetHostEntry(host);
+ foreach (var ip in hostentry.AddressList.Where(ip => ip.AddressFamily == AddressFamily.InterNetwork))
+ {
+ result = ip.ToString();
+ }
+
+ _destination = result;
+ }
+ return _destination;
+ }
+ set
+ {
+ if (_destination != value)
+ {
+ _destination = value;
+ OnPropertyChanged("Destination");
+ }
+ }
+ }
+
+ /// <summary>
+ /// The RemoteEndPoint Address
+ /// </summary>
+ public string Source
+ {
+ get { return _source; }
+ set
+ {
+ if (_source != value)
+ {
+ _source = value;
+ OnPropertyChanged("Source");
+ }
+ }
+ }
+
+ /// <summary>
+ /// The Media Data Delivery RemoteEndPoint Port if we use Unicast
+ /// </summary>
+ public int ServerRtpPort
+ {
+ get
+ {
+ return _serverRtpPort;
+ }
+ set { if (_serverRtpPort != value) { _serverRtpPort = value; OnPropertyChanged("ServerRtpPort"); } }
+ }
+
+ /// <summary>
+ /// The Media Metadata Delivery RemoteEndPoint Port if we use Unicast
+ /// </summary>
+ public int ServerRtcpPort
+ {
+ get { return _serverRtcpPort; }
+ set { if (_serverRtcpPort != value) { _serverRtcpPort = value; OnPropertyChanged("ServerRtcpPort"); } }
+ }
+
+ /// <summary>
+ /// The Media Data Delivery LocalEndPoint Port if we use Unicast
+ /// </summary>
+ public int ClientRtpPort
+ {
+ get { return _clientRtpPort; }
+ set { if (_clientRtpPort != value) { _clientRtpPort = value; OnPropertyChanged("ClientRtpPort"); } }
+ }
+
+ /// <summary>
+ /// The Media Metadata Delivery LocalEndPoint Port if we use Unicast
+ /// </summary>
+ public int ClientRtcpPort
+ {
+ get { return _clientRtcpPort; }
+ set { if (_clientRtcpPort != value) { _clientRtcpPort = value; OnPropertyChanged("ClientRtcpPort"); } }
+ }
+
+ /// <summary>
+ /// The Media Data Delivery RemoteEndPoint Port if we use Multicast
+ /// </summary>
+ public int RtpPort
+ {
+ get { return _rtpPort; }
+ set { if (_rtpPort != value) { _rtpPort = value; OnPropertyChanged("RtpPort"); } }
+ }
+
+ /// <summary>
+ /// The Media Meta Delivery RemoteEndPoint Port if we use Multicast
+ /// </summary>
+ public int RtcpPort
+ {
+ get { return _rtcpPort; }
+ set { if (_rtcpPort != value) { _rtcpPort = value; OnPropertyChanged("RtcpPort"); } }
+ }
+
+ #endregion
+
+ public string Transport
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(_transport))
+ {
+ _transport = "unicast";
+ }
+ return _transport;
+ }
+ set
+ {
+ if (_transport != value)
+ {
+ _transport = value;
+ OnPropertyChanged("Transport");
+ }
+ }
+ }
+ public int SignalLevel
+ {
+ get { return _signalLevel; }
+ set { if (_signalLevel != value) { _signalLevel = value; OnPropertyChanged("SignalLevel"); } }
+ }
+ public int SignalQuality
+ {
+ get { return _signalQuality; }
+ set { if (_signalQuality != value) { _signalQuality = value; OnPropertyChanged("SignalQuality"); } }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private void ProcessSessionHeader(string sessionHeader, string response)
+ {
+ if (!string.IsNullOrEmpty(sessionHeader))
+ {
+ var m = RegexRtspSessionHeader.Match(sessionHeader);
+ if (!m.Success)
+ {
+ _logger.Error("Failed to tune, RTSP {0} response session header {1} format not recognised", response, sessionHeader);
+ }
+ _rtspSessionId = m.Groups[1].Captures[0].Value;
+ _rtspSessionTimeToLive = m.Groups[3].Captures.Count == 1 ? int.Parse(m.Groups[3].Captures[0].Value) : DefaultRtspSessionTimeout;
+ }
+ }
+ private void ProcessTransportHeader(string transportHeader)
+ {
+ if (!string.IsNullOrEmpty(transportHeader))
+ {
+ var transports = transportHeader.Split(',');
+ foreach (var transport in transports)
+ {
+ if (transport.Trim().StartsWith("RTP/AVP"))
+ {
+ var sections = transport.Split(';');
+ foreach (var section in sections)
+ {
+ var parts = section.Split('=');
+ if (parts[0].Equals("server_port"))
+ {
+ var ports = parts[1].Split('-');
+ _serverRtpPort = int.Parse(ports[0]);
+ _serverRtcpPort = int.Parse(ports[1]);
+ }
+ else if (parts[0].Equals("destination"))
+ {
+ _destination = parts[1];
+ }
+ else if (parts[0].Equals("port"))
+ {
+ var ports = parts[1].Split('-');
+ _rtpPort = int.Parse(ports[0]);
+ _rtcpPort = int.Parse(ports[1]);
+ }
+ else if (parts[0].Equals("ttl"))
+ {
+ _rtspSessionTimeToLive = int.Parse(parts[1]);
+ }
+ else if (parts[0].Equals("source"))
+ {
+ _source = parts[1];
+ }
+ else if (parts[0].Equals("client_port"))
+ {
+ var ports = parts[1].Split('-');
+ var rtp = int.Parse(ports[0]);
+ var rtcp = int.Parse(ports[1]);
+ //if (!rtp.Equals(_rtpPort))
+ //{
+ // Logger.Error("SAT>IP base: server specified RTP client port {0} instead of {1}", rtp, _rtpPort);
+ //}
+ //if (!rtcp.Equals(_rtcpPort))
+ //{
+ // Logger.Error("SAT>IP base: server specified RTCP client port {0} instead of {1}", rtcp, _rtcpPort);
+ //}
+ _rtpPort = rtp;
+ _rtcpPort = rtcp;
+ }
+ }
+ }
+ }
+ }
+ }
+ private void Connect()
+ {
+ _rtspSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ var ip = IPAddress.Parse(_address);
+ var rtspEndpoint = new IPEndPoint(ip, 554);
+ _rtspSocket.Connect(rtspEndpoint);
+ }
+ private void Disconnect()
+ {
+ if (_rtspSocket != null && _rtspSocket.Connected)
+ {
+ _rtspSocket.Shutdown(SocketShutdown.Both);
+ _rtspSocket.Close();
+ }
+ }
+ private void SendRequest(RtspRequest request)
+ {
+ if (_rtspSocket == null)
+ {
+ Connect();
+ }
+ try
+ {
+ request.Headers.Add("CSeq", _rtspSequenceNum.ToString());
+ _rtspSequenceNum++;
+ byte[] requestBytes = request.Serialise();
+ if (_rtspSocket != null)
+ {
+ var requestBytesCount = _rtspSocket.Send(requestBytes, requestBytes.Length, SocketFlags.None);
+ if (requestBytesCount < 1)
+ {
+
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.Error(e.Message);
+ }
+ }
+ private void ReceiveResponse(out RtspResponse response)
+ {
+ response = null;
+ var responseBytesCount = 0;
+ byte[] responseBytes = new byte[1024];
+ try
+ {
+ responseBytesCount = _rtspSocket.Receive(responseBytes, responseBytes.Length, SocketFlags.None);
+ response = RtspResponse.Deserialise(responseBytes, responseBytesCount);
+ string contentLengthString;
+ int contentLength = 0;
+ if (response.Headers.TryGetValue("Content-Length", out contentLengthString))
+ {
+ contentLength = int.Parse(contentLengthString);
+ if ((string.IsNullOrEmpty(response.Body) && contentLength > 0) || response.Body.Length < contentLength)
+ {
+ if (response.Body == null)
+ {
+ response.Body = string.Empty;
+ }
+ while (responseBytesCount > 0 && response.Body.Length < contentLength)
+ {
+ responseBytesCount = _rtspSocket.Receive(responseBytes, responseBytes.Length, SocketFlags.None);
+ response.Body += System.Text.Encoding.UTF8.GetString(responseBytes, 0, responseBytesCount);
+ }
+ }
+ }
+ }
+ catch (SocketException)
+ {
+ }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public RtspStatusCode Setup(string query, string transporttype)
+ {
+
+ RtspRequest request;
+ RtspResponse response;
+ //_rtspClient = new RtspClient(_rtspDevice.ServerAddress);
+ if ((_rtspSocket == null))
+ {
+ Connect();
+ }
+ if (string.IsNullOrEmpty(_rtspSessionId))
+ {
+ request = new RtspRequest(RtspMethod.Setup, string.Format("rtsp://{0}:{1}/?{2}", _address, 554, query), 1, 0);
+ switch (transporttype)
+ {
+ case "multicast":
+ request.Headers.Add("Transport", string.Format("RTP/AVP;multicast"));
+ break;
+ case "unicast":
+ var activeTcpConnections = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections();
+ var usedPorts = new HashSet<int>();
+ foreach (var connection in activeTcpConnections)
+ {
+ usedPorts.Add(connection.LocalEndPoint.Port);
+ }
+ for (var port = 40000; port <= 65534; port += 2)
+ {
+ if (!usedPorts.Contains(port) && !usedPorts.Contains(port + 1))
+ {
+
+ _clientRtpPort = port;
+ _clientRtcpPort = port + 1;
+ break;
+ }
+ }
+ request.Headers.Add("Transport", string.Format("RTP/AVP;unicast;client_port={0}-{1}", _clientRtpPort, _clientRtcpPort));
+ break;
+ }
+ }
+ else
+ {
+ request = new RtspRequest(RtspMethod.Setup, string.Format("rtsp://{0}:{1}/?{2}", _address, 554, query), 1, 0);
+ switch (transporttype)
+ {
+ case "multicast":
+ request.Headers.Add("Transport", string.Format("RTP/AVP;multicast"));
+ break;
+ case "unicast":
+ request.Headers.Add("Transport", string.Format("RTP/AVP;unicast;client_port={0}-{1}", _clientRtpPort, _clientRtcpPort));
+ break;
+ }
+
+ }
+ SendRequest(request);
+ ReceiveResponse(out response);
+
+ //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok)
+ //{
+ // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase);
+ //}
+ if (!response.Headers.TryGetValue("com.ses.streamID", out _rtspStreamId))
+ {
+ _logger.Error(string.Format("Failed to tune, not able to locate Stream ID header in RTSP SETUP response"));
+ }
+ string sessionHeader;
+ if (!response.Headers.TryGetValue("Session", out sessionHeader))
+ {
+ _logger.Error(string.Format("Failed to tune, not able to locate Session header in RTSP SETUP response"));
+ }
+ ProcessSessionHeader(sessionHeader, "Setup");
+ string transportHeader;
+ if (!response.Headers.TryGetValue("Transport", out transportHeader))
+ {
+ _logger.Error(string.Format("Failed to tune, not able to locate Transport header in RTSP SETUP response"));
+ }
+ ProcessTransportHeader(transportHeader);
+ return response.StatusCode;
+ }
+
+ public RtspStatusCode Play(string query)
+ {
+ if ((_rtspSocket == null))
+ {
+ Connect();
+ }
+ //_rtspClient = new RtspClient(_rtspDevice.ServerAddress);
+ RtspResponse response;
+ string data;
+ if (string.IsNullOrEmpty(query))
+ {
+ data = string.Format("rtsp://{0}:{1}/stream={2}", _address,
+ 554, _rtspStreamId);
+ }
+ else
+ {
+ data = string.Format("rtsp://{0}:{1}/stream={2}?{3}", _address,
+ 554, _rtspStreamId, query);
+ }
+ var request = new RtspRequest(RtspMethod.Play, data, 1, 0);
+ request.Headers.Add("Session", _rtspSessionId);
+ SendRequest(request);
+ ReceiveResponse(out response);
+ //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok)
+ //{
+ // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase);
+ //}
+ //Logger.Info("RtspSession-Play : \r\n {0}", response);
+ string sessionHeader;
+ if (!response.Headers.TryGetValue("Session", out sessionHeader))
+ {
+ _logger.Error(string.Format("Failed to tune, not able to locate Session header in RTSP Play response"));
+ }
+ ProcessSessionHeader(sessionHeader, "Play");
+ string rtpinfoHeader;
+ if (!response.Headers.TryGetValue("RTP-Info", out rtpinfoHeader))
+ {
+ _logger.Error(string.Format("Failed to tune, not able to locate Rtp-Info header in RTSP Play response"));
+ }
+ return response.StatusCode;
+ }
+
+ public RtspStatusCode Options()
+ {
+ if ((_rtspSocket == null))
+ {
+ Connect();
+ }
+ //_rtspClient = new RtspClient(_rtspDevice.ServerAddress);
+ RtspRequest request;
+ RtspResponse response;
+
+
+ if (string.IsNullOrEmpty(_rtspSessionId))
+ {
+ request = new RtspRequest(RtspMethod.Options, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0);
+ }
+ else
+ {
+ request = new RtspRequest(RtspMethod.Options, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0);
+ request.Headers.Add("Session", _rtspSessionId);
+ }
+ SendRequest(request);
+ ReceiveResponse(out response);
+ //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok)
+ //{
+ // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase);
+ //}
+ //Logger.Info("RtspSession-Options : \r\n {0}", response);
+ string sessionHeader;
+ if (!response.Headers.TryGetValue("Session", out sessionHeader))
+ {
+ _logger.Error(string.Format("Failed to tune, not able to locate session header in RTSP Options response"));
+ }
+ ProcessSessionHeader(sessionHeader, "Options");
+ string optionsHeader;
+ if (!response.Headers.TryGetValue("Public", out optionsHeader))
+ {
+ _logger.Error(string.Format("Failed to tune, not able to Options header in RTSP Options response"));
+ }
+ return response.StatusCode;
+ }
+
+ public RtspStatusCode Describe(out int level, out int quality)
+ {
+ if ((_rtspSocket == null))
+ {
+ Connect();
+ }
+ //_rtspClient = new RtspClient(_rtspDevice.ServerAddress);
+ RtspRequest request;
+ RtspResponse response;
+ level = 0;
+ quality = 0;
+
+ if (string.IsNullOrEmpty(_rtspSessionId))
+ {
+ request = new RtspRequest(RtspMethod.Describe, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0);
+ request.Headers.Add("Accept", "application/sdp");
+
+ }
+ else
+ {
+ request = new RtspRequest(RtspMethod.Describe, string.Format("rtsp://{0}:{1}/stream={2}", _address, 554, _rtspStreamId), 1, 0);
+ request.Headers.Add("Accept", "application/sdp");
+ request.Headers.Add("Session", _rtspSessionId);
+ }
+ SendRequest(request);
+ ReceiveResponse(out response);
+ //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok)
+ //{
+ // Logger.Error("Failed to tune, non-OK RTSP Describe status code {0} {1}", response.StatusCode, response.ReasonPhrase);
+ //}
+ //Logger.Info("RtspSession-Describe : \r\n {0}", response);
+ string sessionHeader;
+ if (!response.Headers.TryGetValue("Session", out sessionHeader))
+ {
+ _logger.Error(string.Format("Failed to tune, not able to locate session header in RTSP Describe response"));
+ }
+ ProcessSessionHeader(sessionHeader, "Describe");
+ var m = RegexDescribeResponseSignalInfo.Match(response.Body);
+ if (m.Success)
+ {
+
+ //isSignalLocked = m.Groups[2].Captures[0].Value.Equals("1");
+ level = int.Parse(m.Groups[1].Captures[0].Value) * 100 / 255; // level: 0..255 => 0..100
+ quality = int.Parse(m.Groups[3].Captures[0].Value) * 100 / 15; // quality: 0..15 => 0..100
+
+ }
+ /*
+ v=0
+ o=- 1378633020884883 1 IN IP4 192.168.2.108
+ s=SatIPServer:1 4
+ t=0 0
+ a=tool:idl4k
+ m=video 52780 RTP/AVP 33
+ c=IN IP4 0.0.0.0
+ b=AS:5000
+ a=control:stream=4
+ a=fmtp:33 ver=1.0;tuner=1,0,0,0,12344,h,dvbs2,,off,,22000,34;pids=0,100,101,102,103,106
+ =sendonly
+ */
+
+
+ return response.StatusCode;
+ }
+
+ public RtspStatusCode TearDown()
+ {
+ if ((_rtspSocket == null))
+ {
+ Connect();
+ }
+ //_rtspClient = new RtspClient(_rtspDevice.ServerAddress);
+ RtspResponse response;
+
+ var request = new RtspRequest(RtspMethod.Teardown, string.Format("rtsp://{0}:{1}/stream={2}", _address, 554, _rtspStreamId), 1, 0);
+ request.Headers.Add("Session", _rtspSessionId);
+ SendRequest(request);
+ ReceiveResponse(out response);
+ //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok)
+ //{
+ // Logger.Error("Failed to tune, non-OK RTSP Teardown status code {0} {1}", response.StatusCode, response.ReasonPhrase);
+ //}
+ return response.StatusCode;
+ }
+
+ #endregion
+
+ #region Public Events
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ #endregion
+
+ #region Protected Methods
+
+ protected void OnPropertyChanged(string name)
+ {
+ //var handler = PropertyChanged;
+ //if (handler != null)
+ //{
+ // handler(this, new PropertyChangedEventArgs(name));
+ //}
+ }
+
+ #endregion
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);//Disconnect();
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ TearDown();
+ Disconnect();
+ }
+ }
+ _disposed = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs
new file mode 100644
index 000000000..6d6d50623
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs
@@ -0,0 +1,251 @@
+/*
+ Copyright (C) <2007-2016> <Kay Diefenthal>
+
+ SatIp.RtspSample is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ SatIp.RtspSample is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+using System.ComponentModel;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp
+{
+ /// <summary>
+ /// Standard RTSP status codes.
+ /// </summary>
+ public enum RtspStatusCode
+ {
+ /// <summary>
+ /// 100 continue
+ /// </summary>
+ Continue = 100,
+
+ /// <summary>
+ /// 200 OK
+ /// </summary>
+ [Description("Okay")]
+ Ok = 200,
+ /// <summary>
+ /// 201 created
+ /// </summary>
+ Created = 201,
+
+ /// <summary>
+ /// 250 low on storage space
+ /// </summary>
+ [Description("Low On Storage Space")]
+ LowOnStorageSpace = 250,
+
+ /// <summary>
+ /// 300 multiple choices
+ /// </summary>
+ [Description("Multiple Choices")]
+ MultipleChoices = 300,
+ /// <summary>
+ /// 301 moved permanently
+ /// </summary>
+ [Description("Moved Permanently")]
+ MovedPermanently = 301,
+ /// <summary>
+ /// 302 moved temporarily
+ /// </summary>
+ [Description("Moved Temporarily")]
+ MovedTemporarily = 302,
+ /// <summary>
+ /// 303 see other
+ /// </summary>
+ [Description("See Other")]
+ SeeOther = 303,
+ /// <summary>
+ /// 304 not modified
+ /// </summary>
+ [Description("Not Modified")]
+ NotModified = 304,
+ /// <summary>
+ /// 305 use proxy
+ /// </summary>
+ [Description("Use Proxy")]
+ UseProxy = 305,
+
+ /// <summary>
+ /// 400 bad request
+ /// </summary>
+ [Description("Bad Request")]
+ BadRequest = 400,
+ /// <summary>
+ /// 401 unauthorised
+ /// </summary>
+ Unauthorised = 401,
+ /// <summary>
+ /// 402 payment required
+ /// </summary>
+ [Description("Payment Required")]
+ PaymentRequired = 402,
+ /// <summary>
+ /// 403 forbidden
+ /// </summary>
+ Forbidden = 403,
+ /// <summary>
+ /// 404 not found
+ /// </summary>
+ [Description("Not Found")]
+ NotFound = 404,
+ /// <summary>
+ /// 405 method not allowed
+ /// </summary>
+ [Description("Method Not Allowed")]
+ MethodNotAllowed = 405,
+ /// <summary>
+ /// 406 not acceptable
+ /// </summary>
+ [Description("Not Acceptable")]
+ NotAcceptable = 406,
+ /// <summary>
+ /// 407 proxy authentication required
+ /// </summary>
+ [Description("Proxy Authentication Required")]
+ ProxyAuthenticationRequred = 407,
+ /// <summary>
+ /// 408 request time-out
+ /// </summary>
+ [Description("Request Time-Out")]
+ RequestTimeOut = 408,
+
+ /// <summary>
+ /// 410 gone
+ /// </summary>
+ Gone = 410,
+ /// <summary>
+ /// 411 length required
+ /// </summary>
+ [Description("Length Required")]
+ LengthRequired = 411,
+ /// <summary>
+ /// 412 precondition failed
+ /// </summary>
+ [Description("Precondition Failed")]
+ PreconditionFailed = 412,
+ /// <summary>
+ /// 413 request entity too large
+ /// </summary>
+ [Description("Request Entity Too Large")]
+ RequestEntityTooLarge = 413,
+ /// <summary>
+ /// 414 request URI too large
+ /// </summary>
+ [Description("Request URI Too Large")]
+ RequestUriTooLarge = 414,
+ /// <summary>
+ /// 415 unsupported media type
+ /// </summary>
+ [Description("Unsupported Media Type")]
+ UnsupportedMediaType = 415,
+
+ /// <summary>
+ /// 451 parameter not understood
+ /// </summary>
+ [Description("Parameter Not Understood")]
+ ParameterNotUnderstood = 451,
+ /// <summary>
+ /// 452 conference not found
+ /// </summary>
+ [Description("Conference Not Found")]
+ ConferenceNotFound = 452,
+ /// <summary>
+ /// 453 not enough bandwidth
+ /// </summary>
+ [Description("Not Enough Bandwidth")]
+ NotEnoughBandwidth = 453,
+ /// <summary>
+ /// 454 session not found
+ /// </summary>
+ [Description("Session Not Found")]
+ SessionNotFound = 454,
+ /// <summary>
+ /// 455 method not valid in this state
+ /// </summary>
+ [Description("Method Not Valid In This State")]
+ MethodNotValidInThisState = 455,
+ /// <summary>
+ /// 456 header field not valid for this resource
+ /// </summary>
+ [Description("Header Field Not Valid For This Resource")]
+ HeaderFieldNotValidForThisResource = 456,
+ /// <summary>
+ /// 457 invalid range
+ /// </summary>
+ [Description("Invalid Range")]
+ InvalidRange = 457,
+ /// <summary>
+ /// 458 parameter is read-only
+ /// </summary>
+ [Description("Parameter Is Read-Only")]
+ ParameterIsReadOnly = 458,
+ /// <summary>
+ /// 459 aggregate operation not allowed
+ /// </summary>
+ [Description("Aggregate Operation Not Allowed")]
+ AggregateOperationNotAllowed = 459,
+ /// <summary>
+ /// 460 only aggregate operation allowed
+ /// </summary>
+ [Description("Only Aggregate Operation Allowed")]
+ OnlyAggregateOperationAllowed = 460,
+ /// <summary>
+ /// 461 unsupported transport
+ /// </summary>
+ [Description("Unsupported Transport")]
+ UnsupportedTransport = 461,
+ /// <summary>
+ /// 462 destination unreachable
+ /// </summary>
+ [Description("Destination Unreachable")]
+ DestinationUnreachable = 462,
+
+ /// <summary>
+ /// 500 internal server error
+ /// </summary>
+ [Description("Internal Server Error")]
+ InternalServerError = 500,
+ /// <summary>
+ /// 501 not implemented
+ /// </summary>
+ [Description("Not Implemented")]
+ NotImplemented = 501,
+ /// <summary>
+ /// 502 bad gateway
+ /// </summary>
+ [Description("Bad Gateway")]
+ BadGateway = 502,
+ /// <summary>
+ /// 503 service unavailable
+ /// </summary>
+ [Description("Service Unavailable")]
+ ServiceUnavailable = 503,
+ /// <summary>
+ /// 504 gateway time-out
+ /// </summary>
+ [Description("Gateway Time-Out")]
+ GatewayTimeOut = 504,
+ /// <summary>
+ /// 505 RTSP version not supported
+ /// </summary>
+ [Description("RTSP Version Not Supported")]
+ RtspVersionNotSupported = 505,
+
+ /// <summary>
+ /// 551 option not supported
+ /// </summary>
+ [Description("Option Not Supported")]
+ OptionNotSupported = 551
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
index da1894bb7..d0a55966f 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Extensions;
+using System.Xml.Linq;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
{
@@ -171,58 +172,86 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
public async Task<SatIpTunerHostInfo> GetInfo(string url, CancellationToken cancellationToken)
{
+ Uri locationUri = new Uri(url);
+ string devicetype = "";
+ string friendlyname = "";
+ string uniquedevicename = "";
+ string manufacturer = "";
+ string manufacturerurl = "";
+ string modelname = "";
+ string modeldescription = "";
+ string modelnumber = "";
+ string modelurl = "";
+ string serialnumber = "";
+ string presentationurl = "";
+ string capabilities = "";
+ string m3u = "";
+ var document = XDocument.Load(locationUri.AbsoluteUri);
+ var xnm = new XmlNamespaceManager(new NameTable());
+ XNamespace n1 = "urn:ses-com:satip";
+ XNamespace n0 = "urn:schemas-upnp-org:device-1-0";
+ xnm.AddNamespace("root", n0.NamespaceName);
+ xnm.AddNamespace("satip:", n1.NamespaceName);
+ if (document.Root != null)
+ {
+ var deviceElement = document.Root.Element(n0 + "device");
+ if (deviceElement != null)
+ {
+ var devicetypeElement = deviceElement.Element(n0 + "deviceType");
+ if (devicetypeElement != null)
+ devicetype = devicetypeElement.Value;
+ var friendlynameElement = deviceElement.Element(n0 + "friendlyName");
+ if (friendlynameElement != null)
+ friendlyname = friendlynameElement.Value;
+ var manufactureElement = deviceElement.Element(n0 + "manufacturer");
+ if (manufactureElement != null)
+ manufacturer = manufactureElement.Value;
+ var manufactureurlElement = deviceElement.Element(n0 + "manufacturerURL");
+ if (manufactureurlElement != null)
+ manufacturerurl = manufactureurlElement.Value;
+ var modeldescriptionElement = deviceElement.Element(n0 + "modelDescription");
+ if (modeldescriptionElement != null)
+ modeldescription = modeldescriptionElement.Value;
+ var modelnameElement = deviceElement.Element(n0 + "modelName");
+ if (modelnameElement != null)
+ modelname = modelnameElement.Value;
+ var modelnumberElement = deviceElement.Element(n0 + "modelNumber");
+ if (modelnumberElement != null)
+ modelnumber = modelnumberElement.Value;
+ var modelurlElement = deviceElement.Element(n0 + "modelURL");
+ if (modelurlElement != null)
+ modelurl = modelurlElement.Value;
+ var serialnumberElement = deviceElement.Element(n0 + "serialNumber");
+ if (serialnumberElement != null)
+ serialnumber = serialnumberElement.Value;
+ var uniquedevicenameElement = deviceElement.Element(n0 + "UDN");
+ if (uniquedevicenameElement != null) uniquedevicename = uniquedevicenameElement.Value;
+ var presentationUrlElement = deviceElement.Element(n0 + "presentationURL");
+ if (presentationUrlElement != null) presentationurl = presentationUrlElement.Value;
+ var capabilitiesElement = deviceElement.Element(n1 + "X_SATIPCAP");
+ if (capabilitiesElement != null) capabilities = capabilitiesElement.Value;
+ var m3uElement = deviceElement.Element(n1 + "X_SATIPM3U");
+ if (m3uElement != null) m3u = m3uElement.Value;
+ }
+ }
+
var result = new SatIpTunerHostInfo
{
Url = url,
+ Id = uniquedevicename,
IsEnabled = true,
Type = SatIpHost.DeviceType,
Tuners = 1,
- TunersAvailable = 1
+ TunersAvailable = 1,
+ M3UUrl = m3u
};
- 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.DeviceId))
+ result.FriendlyName = friendlyname;
+ if (string.IsNullOrWhiteSpace(result.Id))
{
throw new NotImplementedException();
}
- // Device hasn't implemented an m3u list
- if (string.IsNullOrWhiteSpace(result.M3UUrl))
- {
- result.IsEnabled = false;
- }
-
else if (!result.M3UUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
var fullM3uUrl = url.Substring(0, url.LastIndexOf('/'));
@@ -233,66 +262,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
return result;
}
-
- private void FillFromDeviceNode(SatIpTunerHostInfo info, XmlReader reader)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.LocalName)
- {
- case "UDN":
- {
- info.DeviceId = 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
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
index 46a2a8524..1e571c84f 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
@@ -40,7 +40,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(tuner.M3UUrl, ChannelIdPrefix, tuner.Id, cancellationToken).ConfigureAwait(false);
}
- return new List<ChannelInfo>();
+ var channels = await new ChannelScan(Logger).Scan(tuner, cancellationToken).ConfigureAwait(false);
+ return channels;
}
public static string DeviceType
@@ -163,5 +164,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
return list;
}
+
+ public string ApplyDuration(string streamPath, TimeSpan duration)
+ {
+ return streamPath;
+ }
}
}