aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/LiveTv
diff options
context:
space:
mode:
authorLuke <luke.pulverenti@gmail.com>2015-09-02 11:50:00 -0400
committerLuke <luke.pulverenti@gmail.com>2015-09-02 11:50:00 -0400
commitf868dd81e856488280978006cbb67afc2677049d (patch)
tree616ba8ae846efe9ec889abeb12f6b2702c6b8592 /MediaBrowser.Server.Implementations/LiveTv
parentaf89446c20fb302087b82c18c28da92076dbc5ac (diff)
parente6d5901408ba7d8e344a27ea1f3b0046c40e56c1 (diff)
Merge pull request #1164 from MediaBrowser/dev
3.0.5724.1
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv')
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs12
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs802
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs16
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs118
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs78
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs25
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs138
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListings.cs59
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListingsNorthAmerica.cs366
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/IEmbyListingProvider.cs18
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs982
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs44
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs5
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs387
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs29
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs23
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs218
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs122
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs406
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs199
20 files changed, 3942 insertions, 105 deletions
diff --git a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
index 7c3af0a54..f205da70d 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers;
@@ -17,12 +18,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private readonly ILiveTvManager _liveTvManager;
private readonly IHttpClient _httpClient;
private readonly ILogger _logger;
+ private readonly IApplicationHost _appHost;
- public ChannelImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger)
+ public ChannelImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger, IApplicationHost appHost)
{
_liveTvManager = liveTvManager;
_httpClient = httpClient;
_logger = logger;
+ _appHost = appHost;
}
public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
@@ -46,7 +49,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var options = new HttpRequestOptions
{
CancellationToken = cancellationToken,
- Url = liveTvItem.ProviderImageUrl
+ Url = liveTvItem.ProviderImageUrl,
+
+ // Some image hosts require a user agent to be specified.
+ UserAgent = "Emby Server/" + _appHost.ApplicationVersion
};
var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
new file mode 100644
index 000000000..4e0d6e8d4
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -0,0 +1,802 @@
+using MediaBrowser.Common;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Security;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.FileOrganization;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.FileOrganization;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Server.Implementations.FileOrganization;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ public class EmbyTV : ILiveTvService, IHasRegistrationInfo, IDisposable
+ {
+ private readonly IApplicationHost _appHpst;
+ private readonly ILogger _logger;
+ private readonly IHttpClient _httpClient;
+ private readonly IServerConfigurationManager _config;
+ private readonly IJsonSerializer _jsonSerializer;
+
+ private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
+ private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
+ private readonly TimerManager _timerProvider;
+
+ private readonly LiveTvManager _liveTvManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ISecurityManager _security;
+
+ private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileOrganizationService _organizationService;
+
+ public static EmbyTV Current;
+
+ 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)
+ {
+ Current = this;
+
+ _appHpst = appHost;
+ _logger = logger;
+ _httpClient = httpClient;
+ _config = config;
+ _fileSystem = fileSystem;
+ _security = security;
+ _libraryManager = libraryManager;
+ _libraryMonitor = libraryMonitor;
+ _providerManager = providerManager;
+ _organizationService = organizationService;
+ _liveTvManager = (LiveTvManager)liveTvManager;
+ _jsonSerializer = jsonSerializer;
+
+ _recordingProvider = new ItemDataProvider<RecordingInfo>(jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase));
+ _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
+ _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers"));
+ _timerProvider.TimerFired += _timerProvider_TimerFired;
+ }
+
+ public void Start()
+ {
+ _timerProvider.RestartTimers();
+ }
+
+ public event EventHandler DataSourceChanged;
+
+ public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
+
+ private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeRecordings =
+ new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.OrdinalIgnoreCase);
+
+ public string Name
+ {
+ get { return "Emby"; }
+ }
+
+ public string DataPath
+ {
+ get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
+ }
+
+ public string HomePageUrl
+ {
+ get { return "http://emby.media"; }
+ }
+
+ public async Task<LiveTvServiceStatusInfo> GetStatusInfoAsync(CancellationToken cancellationToken)
+ {
+ var status = new LiveTvServiceStatusInfo();
+ var list = new List<LiveTvTunerInfo>();
+
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
+ {
+ try
+ {
+ var tuners = await hostInstance.GetTunerInfos(cancellationToken).ConfigureAwait(false);
+
+ list.AddRange(tuners);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting tuners", ex);
+ }
+ }
+
+ status.Tuners = list;
+ status.Status = LiveTvServiceStatus.Ok;
+ status.Version = _appHpst.ApplicationVersion.ToString();
+ status.IsVisible = false;
+ return status;
+ }
+
+ private List<ChannelInfo> _channelCache = null;
+ private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
+ {
+ if (enableCache && _channelCache != null)
+ {
+
+ return _channelCache.ToList();
+ }
+
+ 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);
+ }
+ }
+
+ if (list.Count > 0)
+ {
+ foreach (var provider in GetListingProviders())
+ {
+ try
+ {
+ await provider.Item1.AddMetadata(provider.Item2, list, cancellationToken).ConfigureAwait(false);
+ }
+ catch (NotSupportedException)
+ {
+
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error adding metadata", ex);
+ }
+ }
+ }
+ _channelCache = list;
+ return list;
+ }
+
+ public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
+ {
+ return GetChannelsAsync(false, cancellationToken);
+ }
+
+ public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
+ {
+ var timers = _timerProvider.GetAll().Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase));
+ foreach (var timer in timers)
+ {
+ CancelTimerInternal(timer.Id);
+ }
+
+ var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
+ if (remove != null)
+ {
+ _seriesTimerProvider.Delete(remove);
+ }
+ return Task.FromResult(true);
+ }
+
+ private void CancelTimerInternal(string timerId)
+ {
+ var remove = _timerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
+ if (remove != null)
+ {
+ _timerProvider.Delete(remove);
+ }
+ CancellationTokenSource cancellationTokenSource;
+
+ if (_activeRecordings.TryGetValue(timerId, out cancellationTokenSource))
+ {
+ cancellationTokenSource.Cancel();
+ }
+ }
+
+ public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
+ {
+ CancelTimerInternal(timerId);
+ return Task.FromResult(true);
+ }
+
+ public async 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);
+ }
+ }
+
+ try
+ {
+ File.Delete(remove.Path);
+ }
+ catch (DirectoryNotFoundException)
+ {
+
+ }
+ catch (FileNotFoundException)
+ {
+
+ }
+ _recordingProvider.Delete(remove);
+ }
+ }
+
+ public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ {
+ info.Id = Guid.NewGuid().ToString("N");
+ _timerProvider.Add(info);
+ return Task.FromResult(0);
+ }
+
+ public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+ {
+ info.Id = Guid.NewGuid().ToString("N");
+
+ List<ProgramInfo> epgData;
+ if (info.RecordAnyChannel)
+ {
+ var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
+ var channelIds = channels.Select(i => i.Id).ToList();
+ epgData = GetEpgDataForChannels(channelIds);
+ }
+ else
+ {
+ epgData = GetEpgDataForChannel(info.ChannelId);
+ }
+
+ // populate info.seriesID
+ var program = epgData.FirstOrDefault(i => string.Equals(i.Id, info.ProgramId, StringComparison.OrdinalIgnoreCase));
+
+ if (program != null)
+ {
+ info.SeriesId = program.SeriesId;
+ }
+ else
+ {
+ throw new InvalidOperationException("SeriesId for program not found");
+ }
+
+ _seriesTimerProvider.Add(info);
+ await UpdateTimersForSeriesTimer(epgData, info).ConfigureAwait(false);
+ }
+
+ public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+ {
+ _seriesTimerProvider.Update(info);
+ List<ProgramInfo> epgData;
+ if (info.RecordAnyChannel)
+ {
+ var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
+ var channelIds = channels.Select(i => i.Id).ToList();
+ epgData = GetEpgDataForChannels(channelIds);
+ }
+ else
+ {
+ epgData = GetEpgDataForChannel(info.ChannelId);
+ }
+
+ await UpdateTimersForSeriesTimer(epgData, info).ConfigureAwait(false);
+ }
+
+ public Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ {
+ _timerProvider.Update(info);
+ return Task.FromResult(true);
+ }
+
+ public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task<ImageStream> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task<ImageStream> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult((IEnumerable<RecordingInfo>)_recordingProvider.GetAll());
+ }
+
+ public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult((IEnumerable<TimerInfo>)_timerProvider.GetAll());
+ }
+
+ public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
+ {
+ var config = GetConfiguration();
+
+ var defaults = new SeriesTimerInfo()
+ {
+ PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
+ PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
+ RecordAnyChannel = false,
+ RecordAnyTime = false,
+ RecordNewOnly = false
+ };
+
+ if (program != null)
+ {
+ defaults.SeriesId = program.SeriesId;
+ defaults.ProgramId = program.Id;
+ }
+
+ return Task.FromResult(defaults);
+ }
+
+ public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
+ }
+
+ public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+ {
+ var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
+ var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
+
+ foreach (var provider in GetListingProviders())
+ {
+ var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, startDateUtc, endDateUtc, cancellationToken)
+ .ConfigureAwait(false);
+ var list = programs.ToList();
+
+ // Replace the value that came from the provider with a normalized value
+ foreach (var program in list)
+ {
+ program.ChannelId = channelId;
+ }
+
+ if (list.Count > 0)
+ {
+ SaveEpgDataForChannel(channelId, list);
+
+ return list;
+ }
+ }
+
+ return new List<ProgramInfo>();
+ }
+
+ private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
+ {
+ return GetConfiguration().ListingProviders
+ .Select(i =>
+ {
+ var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+
+ return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
+ })
+ .Where(i => i != null)
+ .ToList();
+ }
+
+ public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
+ {
+ _logger.Info("Streaming Channel " + channelId);
+
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
+ {
+ MediaSourceInfo mediaSourceInfo = null;
+ try
+ {
+ mediaSourceInfo = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ _logger.ErrorException("Error getting channel stream", e);
+ }
+
+ if (mediaSourceInfo != null)
+ {
+ mediaSourceInfo.Id = Guid.NewGuid().ToString("N");
+ return mediaSourceInfo;
+ }
+ }
+
+ throw new ApplicationException("Tuner not found.");
+ }
+
+ public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
+ {
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
+ {
+ try
+ {
+ var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).ConfigureAwait(false);
+
+ if (sources.Count > 0)
+ {
+ return sources;
+ }
+ }
+ catch (NotImplementedException)
+ {
+
+ }
+ }
+
+ throw new NotImplementedException();
+ }
+
+ public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task CloseLiveStream(string id, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(0);
+ }
+
+ public Task RecordLiveStream(string id, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(0);
+ }
+
+ public Task ResetTuner(string id, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(0);
+ }
+
+ async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
+ {
+ var timer = e.Argument;
+
+ _logger.Info("Recording timer fired.");
+
+ try
+ {
+ var cancellationTokenSource = new CancellationTokenSource();
+
+ if (_activeRecordings.TryAdd(timer.Id, cancellationTokenSource))
+ {
+ await RecordStream(timer, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error recording stream", ex);
+
+ if (DateTime.UtcNow < timer.EndDate)
+ {
+ const int retryIntervalSeconds = 60;
+ _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
+
+ _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds));
+ }
+ }
+ }
+
+ private async Task RecordStream(TimerInfo timer, CancellationToken cancellationToken)
+ {
+ if (timer == null)
+ {
+ throw new ArgumentNullException("timer");
+ }
+
+ var mediaStreamInfo = await GetChannelStream(timer.ChannelId, null, CancellationToken.None);
+ var duration = (timer.EndDate - DateTime.UtcNow).Add(TimeSpan.FromSeconds(timer.PostPaddingSeconds));
+
+ HttpRequestOptions httpRequestOptions = new HttpRequestOptions()
+ {
+ Url = mediaStreamInfo.Path
+ };
+
+ var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
+ var recordPath = RecordingPath;
+
+ if (info.IsMovie)
+ {
+ recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name));
+ }
+ else if (info.IsSeries)
+ {
+ recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name));
+ }
+ else if (info.IsKids)
+ {
+ recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name));
+ }
+ else if (info.IsSports)
+ {
+ recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name));
+ }
+ else
+ {
+ recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name));
+ }
+
+ var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)) + ".ts";
+
+ recordPath = Path.Combine(recordPath, recordingFileName);
+ Directory.CreateDirectory(Path.GetDirectoryName(recordPath));
+
+ var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.ProgramId, info.Id, StringComparison.OrdinalIgnoreCase));
+
+ if (recording == null)
+ {
+ recording = new RecordingInfo
+ {
+ ChannelId = info.ChannelId,
+ Id = Guid.NewGuid().ToString("N"),
+ 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,
+ HasImage = info.HasImage,
+ 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.Add(recording);
+ }
+
+ recording.Path = recordPath;
+ recording.Status = RecordingStatus.InProgress;
+ recording.DateLastUpdated = DateTime.UtcNow;
+ _recordingProvider.Update(recording);
+
+ _logger.Info("Beginning recording.");
+
+ try
+ {
+ httpRequestOptions.BufferContent = false;
+ var durationToken = new CancellationTokenSource(duration);
+ var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ httpRequestOptions.CancellationToken = linkedToken;
+ _logger.Info("Writing file to path: " + recordPath);
+ using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET"))
+ {
+ using (var output = File.Open(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken);
+ }
+ }
+
+ recording.Status = RecordingStatus.Completed;
+ _logger.Info("Recording completed");
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.Info("Recording stopped");
+ recording.Status = RecordingStatus.Completed;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error recording", ex);
+ recording.Status = RecordingStatus.Error;
+ }
+
+ recording.DateLastUpdated = DateTime.UtcNow;
+ _recordingProvider.Update(recording);
+ _timerProvider.Delete(timer);
+ _logger.Info("Recording was a success");
+
+ if (recording.Status == RecordingStatus.Completed)
+ {
+ OnSuccessfulRecording(recording);
+ }
+ }
+
+ private async void OnSuccessfulRecording(RecordingInfo recording)
+ {
+ if (GetConfiguration().EnableAutoOrganize)
+ {
+ if (recording.IsSeries)
+ {
+ try
+ {
+ 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);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error processing new recording", ex);
+ }
+ }
+ }
+ }
+
+ private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
+ {
+ var epgData = GetEpgDataForChannel(channelId);
+ return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ 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");
+ }
+
+ private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer)
+ {
+ var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
+
+ if (registration.IsValid)
+ {
+ var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList();
+
+ foreach (var timer in newTimers)
+ {
+ _timerProvider.AddOrUpdate(timer);
+ }
+ }
+ }
+
+ private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms, IReadOnlyList<RecordingInfo> currentRecordings)
+ {
+ // Exclude programs that have already ended
+ allPrograms = allPrograms.Where(i => i.EndDate > 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));
+
+ return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
+ }
+
+ private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
+ {
+ if (!seriesTimer.RecordAnyTime)
+ {
+ allPrograms = allPrograms.Where(epg => (seriesTimer.StartDate.TimeOfDay == epg.StartDate.TimeOfDay));
+ }
+
+ if (seriesTimer.RecordNewOnly)
+ {
+ allPrograms = allPrograms.Where(epg => !epg.IsRepeat);
+ }
+
+ if (!seriesTimer.RecordAnyChannel)
+ {
+ 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");
+ return new List<ProgramInfo>();
+ }
+
+ return allPrograms.Where(i => string.Equals(i.SeriesId, seriesTimer.SeriesId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private string GetChannelEpgCachePath(string channelId)
+ {
+ return Path.Combine(DataPath, "epg", channelId + ".json");
+ }
+
+ private readonly object _epgLock = new object();
+ private void SaveEpgDataForChannel(string channelId, List<ProgramInfo> epgData)
+ {
+ var path = GetChannelEpgCachePath(channelId);
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ lock (_epgLock)
+ {
+ _jsonSerializer.SerializeToFile(epgData, path);
+ }
+ }
+ private List<ProgramInfo> GetEpgDataForChannel(string channelId)
+ {
+ try
+ {
+ lock (_epgLock)
+ {
+ return _jsonSerializer.DeserializeFromFile<List<ProgramInfo>>(GetChannelEpgCachePath(channelId));
+ }
+ }
+ catch
+ {
+ return new List<ProgramInfo>();
+ }
+ }
+ private List<ProgramInfo> GetEpgDataForChannels(List<string> channelIds)
+ {
+ return channelIds.SelectMany(GetEpgDataForChannel).ToList();
+ }
+
+ public void Dispose()
+ {
+ foreach (var pair in _activeRecordings.ToList())
+ {
+ pair.Value.Cancel();
+ }
+ }
+
+ public Task<MBRegistrationRecord> GetRegistrationInfo(string feature)
+ {
+ if (string.Equals(feature, "seriesrecordings", StringComparison.OrdinalIgnoreCase))
+ {
+ return _security.GetRegistrationStatus("embytvseriesrecordings");
+ }
+
+ return Task.FromResult(new MBRegistrationRecord
+ {
+ IsValid = true,
+ IsRegistered = true
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
new file mode 100644
index 000000000..713cb9cd3
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
@@ -0,0 +1,16 @@
+using MediaBrowser.Controller.Plugins;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ public class EntryPoint : IServerEntryPoint
+ {
+ public void Run()
+ {
+ EmbyTV.Current.Start();
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
new file mode 100644
index 000000000..75dec5f97
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -0,0 +1,118 @@
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ public class ItemDataProvider<T>
+ where T : class
+ {
+ private readonly object _fileDataLock = new object();
+ private List<T> _items;
+ private readonly IJsonSerializer _jsonSerializer;
+ protected readonly ILogger Logger;
+ private readonly string _dataPath;
+ protected readonly Func<T, T, bool> EqualityComparer;
+
+ public ItemDataProvider(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer)
+ {
+ Logger = logger;
+ _dataPath = dataPath;
+ EqualityComparer = equalityComparer;
+ _jsonSerializer = jsonSerializer;
+ }
+
+ public IReadOnlyList<T> GetAll()
+ {
+ if (_items == null)
+ {
+ lock (_fileDataLock)
+ {
+ if (_items == null)
+ {
+ _items = GetItemsFromFile(_dataPath);
+ }
+ }
+ }
+ return _items;
+ }
+
+ private List<T> GetItemsFromFile(string path)
+ {
+ var jsonFile = path + ".json";
+
+ try
+ {
+ return _jsonSerializer.DeserializeFromFile<List<T>>(jsonFile);
+ }
+ catch (FileNotFoundException)
+ {
+ }
+ catch (DirectoryNotFoundException ex)
+ {
+ }
+ catch (IOException ex)
+ {
+ Logger.ErrorException("Error deserializing {0}", ex, jsonFile);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error deserializing {0}", ex, jsonFile);
+ }
+ return new List<T>();
+ }
+
+ private void UpdateList(List<T> newList)
+ {
+ var file = _dataPath + ".json";
+ Directory.CreateDirectory(Path.GetDirectoryName(file));
+
+ lock (_fileDataLock)
+ {
+ _jsonSerializer.SerializeToFile(newList, file);
+ _items = newList;
+ }
+ }
+
+ public virtual void Update(T item)
+ {
+ var list = GetAll().ToList();
+
+ var index = list.FindIndex(i => EqualityComparer(i, item));
+
+ if (index == -1)
+ {
+ throw new ArgumentException("item not found");
+ }
+
+ list[index] = item;
+
+ UpdateList(list);
+ }
+
+ public virtual void Add(T item)
+ {
+ var list = GetAll().ToList();
+
+ if (list.Any(i => EqualityComparer(i, item)))
+ {
+ throw new ArgumentException("item already exists");
+ }
+
+ list.Add(item);
+
+ UpdateList(list);
+ }
+
+ public virtual void Delete(T item)
+ {
+ var list = GetAll().Where(i => !EqualityComparer(i, item)).ToList();
+
+ UpdateList(list);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
new file mode 100644
index 000000000..5b83d63b1
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -0,0 +1,78 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.LiveTv;
+using System;
+using System.Globalization;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ internal class RecordingHelper
+ {
+ public static DateTime GetStartTime(TimerInfo timer)
+ {
+ return timer.StartDate.AddSeconds(-timer.PrePaddingSeconds);
+ }
+
+ public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo series)
+ {
+ var timer = new TimerInfo();
+
+ timer.ChannelId = parent.ChannelId;
+ timer.Id = (series.Id + parent.Id).GetMD5().ToString("N");
+ timer.StartDate = parent.StartDate;
+ timer.EndDate = parent.EndDate;
+ timer.ProgramId = parent.Id;
+ timer.PrePaddingSeconds = series.PrePaddingSeconds;
+ timer.PostPaddingSeconds = series.PostPaddingSeconds;
+ timer.IsPostPaddingRequired = series.IsPostPaddingRequired;
+ timer.IsPrePaddingRequired = series.IsPrePaddingRequired;
+ timer.Priority = series.Priority;
+ timer.Name = parent.Name;
+ timer.Overview = parent.Overview;
+ timer.SeriesTimerId = series.Id;
+
+ return timer;
+ }
+
+ public static string GetRecordingName(TimerInfo timer, ProgramInfo info)
+ {
+ if (info == null)
+ {
+ return timer.ProgramId;
+ }
+
+ var name = info.Name;
+
+ if (info.IsSeries)
+ {
+ var addHyphen = true;
+
+ if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue)
+ {
+ name += string.Format(" S{0}E{1}", info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
+ addHyphen = false;
+ }
+ else if (info.OriginalAirDate.HasValue)
+ {
+ name += " " + info.OriginalAirDate.Value.ToString("yyyy-MM-dd");
+ }
+
+ if (addHyphen)
+ {
+ name += " -";
+ }
+
+ if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
+ {
+ name += " " + info.EpisodeTitle;
+ }
+ }
+
+ else if (info.ProductionYear != null)
+ {
+ name += " (" + info.ProductionYear + ")";
+ }
+
+ return name;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
new file mode 100644
index 000000000..eab278eb4
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
+ {
+ public SeriesTimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
+ : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+ {
+ }
+
+ public override void Add(SeriesTimerInfo item)
+ {
+ if (string.IsNullOrWhiteSpace(item.Id))
+ {
+ throw new ArgumentException("SeriesTimerInfo.Id cannot be null or empty.");
+ }
+
+ base.Add(item);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
new file mode 100644
index 000000000..3ae38f382
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -0,0 +1,138 @@
+using MediaBrowser.Common.Events;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ public class TimerManager : ItemDataProvider<TimerInfo>
+ {
+ private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
+
+ public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
+
+ public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
+ : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+ {
+ }
+
+ public void RestartTimers()
+ {
+ StopTimers();
+
+ foreach (var item in GetAll().ToList())
+ {
+ AddTimer(item);
+ }
+ }
+
+ public void StopTimers()
+ {
+ foreach (var pair in _timers.ToList())
+ {
+ pair.Value.Dispose();
+ }
+
+ _timers.Clear();
+ }
+
+ public override void Delete(TimerInfo item)
+ {
+ base.Delete(item);
+ StopTimer(item);
+ }
+
+ public override void Update(TimerInfo item)
+ {
+ base.Update(item);
+
+ Timer timer;
+ if (_timers.TryGetValue(item.Id, out timer))
+ {
+ var timespan = RecordingHelper.GetStartTime(item) - DateTime.UtcNow;
+ timer.Change(timespan, TimeSpan.Zero);
+ }
+ else
+ {
+ AddTimer(item);
+ }
+ }
+
+ public override void Add(TimerInfo item)
+ {
+ if (string.IsNullOrWhiteSpace(item.Id))
+ {
+ throw new ArgumentException("TimerInfo.Id cannot be null or empty.");
+ }
+
+ base.Add(item);
+ AddTimer(item);
+ }
+
+ public void AddOrUpdate(TimerInfo item)
+ {
+ var list = GetAll().ToList();
+
+ if (!list.Any(i => EqualityComparer(i, item)))
+ {
+ Add(item);
+ }
+ else
+ {
+ Update(item);
+ }
+ }
+
+ private void AddTimer(TimerInfo item)
+ {
+ var startDate = RecordingHelper.GetStartTime(item);
+ var now = DateTime.UtcNow;
+
+ if (startDate < now)
+ {
+ EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = item }, Logger);
+ return;
+ }
+
+ var timerLength = startDate - now;
+ StartTimer(item, timerLength);
+ }
+
+ public void StartTimer(TimerInfo item, TimeSpan length)
+ {
+ StopTimer(item);
+
+ var timer = new Timer(TimerCallback, item.Id, length, TimeSpan.Zero);
+
+ if (!_timers.TryAdd(item.Id, timer))
+ {
+ timer.Dispose();
+ }
+ }
+
+ private void StopTimer(TimerInfo item)
+ {
+ Timer timer;
+ if (_timers.TryRemove(item.Id, out timer))
+ {
+ timer.Dispose();
+ }
+ }
+
+ private void TimerCallback(object state)
+ {
+ var timerId = (string)state;
+
+ var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
+ if (timer != null)
+ {
+ EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = timer }, Logger);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListings.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListings.cs
new file mode 100644
index 000000000..e446ff469
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListings.cs
@@ -0,0 +1,59 @@
+using MediaBrowser.Common.Net;
+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;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.Listings.Emby
+{
+ public class EmbyGuide : IListingsProvider
+ {
+ private readonly IHttpClient _httpClient;
+ private readonly IJsonSerializer _jsonSerializer;
+
+ public EmbyGuide(IHttpClient httpClient, IJsonSerializer jsonSerializer)
+ {
+ _httpClient = httpClient;
+ _jsonSerializer = jsonSerializer;
+ }
+
+ public string Name
+ {
+ get { return "Emby Guide"; }
+ }
+
+ public string Type
+ {
+ get { return "emby"; }
+ }
+
+ public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+ {
+ return GetListingsProvider(info.Country).GetProgramsAsync(info, channelNumber, startDateUtc, endDateUtc, cancellationToken);
+ }
+
+ public Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken)
+ {
+ return GetListingsProvider(info.Country).AddMetadata(info, channels, cancellationToken);
+ }
+
+ public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
+ {
+ return GetListingsProvider(info.Country).Validate(info, validateLogin, validateListings);
+ }
+
+ public Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
+ {
+ return GetListingsProvider(country).GetLineups(country, location);
+ }
+
+ private IEmbyListingProvider GetListingsProvider(string country)
+ {
+ return new EmbyListingsNorthAmerica(_httpClient, _jsonSerializer);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListingsNorthAmerica.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListingsNorthAmerica.cs
new file mode 100644
index 000000000..2993740d6
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListingsNorthAmerica.cs
@@ -0,0 +1,366 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.Listings.Emby
+{
+ public class EmbyListingsNorthAmerica : IEmbyListingProvider
+ {
+ private readonly IHttpClient _httpClient;
+ private readonly IJsonSerializer _jsonSerializer;
+
+ public EmbyListingsNorthAmerica(IHttpClient httpClient, IJsonSerializer jsonSerializer)
+ {
+ _httpClient = httpClient;
+ _jsonSerializer = jsonSerializer;
+ }
+
+ public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+ {
+ channelNumber = NormalizeNumber(channelNumber);
+
+ var url = "https://data.emby.media/service/listings?id=" + info.ListingsId;
+
+ // Normalize
+ startDateUtc = startDateUtc.Date;
+ endDateUtc = startDateUtc.AddDays(7);
+
+ url += "&start=" + startDateUtc.ToString("s", CultureInfo.InvariantCulture) + "Z";
+ url += "&end=" + endDateUtc.ToString("s", CultureInfo.InvariantCulture) + "Z";
+
+ var response = await GetResponse<ListingInfo[]>(url).ConfigureAwait(false);
+
+ return response.Where(i => IncludeInResults(i, channelNumber)).Select(GetProgramInfo).OrderBy(i => i.StartDate);
+ }
+
+ private ProgramInfo GetProgramInfo(ListingInfo info)
+ {
+ var showType = info.showType ?? string.Empty;
+
+ var program = new ProgramInfo
+ {
+ Id = info.listingID.ToString(CultureInfo.InvariantCulture),
+ Name = GetStringValue(info.showName),
+ HomePageUrl = GetStringValue(info.webLink),
+ Overview = info.description,
+ IsHD = info.hd,
+ IsLive = info.live,
+ IsPremiere = info.seasonPremiere || info.seriesPremiere,
+ IsMovie = showType.IndexOf("Movie", StringComparison.OrdinalIgnoreCase) != -1,
+ IsKids = showType.IndexOf("Children", StringComparison.OrdinalIgnoreCase) != -1,
+ IsNews = showType.IndexOf("News", StringComparison.OrdinalIgnoreCase) != -1,
+ IsSports = showType.IndexOf("Sports", StringComparison.OrdinalIgnoreCase) != -1
+ };
+
+ if (!string.IsNullOrWhiteSpace(info.listDateTime))
+ {
+ program.StartDate = DateTime.ParseExact(info.listDateTime, "yyyy'-'MM'-'dd' 'HH':'mm':'ss", CultureInfo.InvariantCulture);
+ program.StartDate = DateTime.SpecifyKind(program.StartDate, DateTimeKind.Utc);
+ program.EndDate = program.StartDate.AddMinutes(info.duration);
+ }
+
+ if (info.starRating > 0)
+ {
+ program.CommunityRating = info.starRating*2;
+ }
+
+ if (!string.IsNullOrWhiteSpace(info.rating))
+ {
+ // They don't have dashes so try to normalize
+ program.OfficialRating = info.rating.Replace("TV", "TV-").Replace("--", "-");
+
+ var invalid = new[] { "N/A", "Approved", "Not Rated" };
+ if (invalid.Contains(program.OfficialRating, StringComparer.OrdinalIgnoreCase))
+ {
+ program.OfficialRating = null;
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(info.year))
+ {
+ program.ProductionYear = int.Parse(info.year, CultureInfo.InvariantCulture);
+ }
+
+ if (info.showID > 0)
+ {
+ program.ShowId = info.showID.ToString(CultureInfo.InvariantCulture);
+ }
+
+ if (info.seriesID > 0)
+ {
+ program.SeriesId = info.seriesID.ToString(CultureInfo.InvariantCulture);
+ program.IsSeries = true;
+ program.IsRepeat = info.repeat;
+
+ program.EpisodeTitle = GetStringValue(info.episodeTitle);
+
+ if (string.Equals(program.Name, program.EpisodeTitle, StringComparison.OrdinalIgnoreCase))
+ {
+ program.EpisodeTitle = null;
+ }
+ }
+
+ if (info.starRating > 0)
+ {
+ program.CommunityRating = info.starRating * 2;
+ }
+
+ if (string.Equals(info.showName, "Movie", StringComparison.OrdinalIgnoreCase))
+ {
+ // Sometimes the movie title will be in here
+ if (!string.IsNullOrWhiteSpace(info.episodeTitle))
+ {
+ program.Name = info.episodeTitle;
+ }
+ }
+
+ return program;
+ }
+
+ private string GetStringValue(string s)
+ {
+ return string.IsNullOrWhiteSpace(s) ? null : s;
+ }
+
+ private bool IncludeInResults(ListingInfo info, string itemNumber)
+ {
+ if (string.Equals(itemNumber, NormalizeNumber(info.number), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ var channelNumber = info.channelNumber.ToString(CultureInfo.InvariantCulture);
+ if (info.subChannelNumber > 0)
+ {
+ channelNumber += "." + info.subChannelNumber.ToString(CultureInfo.InvariantCulture);
+ }
+
+ return string.Equals(channelNumber, itemNumber, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken)
+ {
+ var response = await GetResponse<LineupDetailResponse>("https://data.emby.media/service/lineups?id=" + info.ListingsId).ConfigureAwait(false);
+
+ foreach (var channel in channels)
+ {
+ var station = response.stations.FirstOrDefault(i =>
+ {
+ var itemNumber = NormalizeNumber(channel.Number);
+
+ if (string.Equals(itemNumber, NormalizeNumber(i.number), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ var channelNumber = i.channelNumber.ToString(CultureInfo.InvariantCulture);
+ if (i.subChannelNumber > 0)
+ {
+ channelNumber += "." + i.subChannelNumber.ToString(CultureInfo.InvariantCulture);
+ }
+
+ return string.Equals(channelNumber, itemNumber, StringComparison.OrdinalIgnoreCase);
+ });
+
+ if (station != null)
+ {
+ //channel.Name = station.name;
+
+ if (!string.IsNullOrWhiteSpace(station.logoFilename))
+ {
+ channel.HasImage = true;
+ channel.ImageUrl = "http://cdn.tvpassport.com/image/station/100x100/" + station.logoFilename;
+ }
+ }
+ }
+ }
+
+ private string NormalizeNumber(string number)
+ {
+ return number.Replace('-', '.');
+ }
+
+ public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
+ {
+ return Task.FromResult(true);
+ }
+
+ public async Task<List<NameIdPair>> GetLineups(string country, string location)
+ {
+ // location = postal code
+ var response = await GetResponse<LineupInfo[]>("https://data.emby.media/service/lineups?postalCode=" + location).ConfigureAwait(false);
+
+ return response.Select(i => new NameIdPair
+ {
+ Name = GetName(i),
+ Id = i.lineupID
+
+ }).OrderBy(i => i.Name).ToList();
+ }
+
+ private string GetName(LineupInfo info)
+ {
+ var name = info.lineupName;
+
+ if (string.Equals(info.lineupType, "cab", StringComparison.OrdinalIgnoreCase))
+ {
+ name += " - Cable";
+ }
+ else if (string.Equals(info.lineupType, "sat", StringComparison.OrdinalIgnoreCase))
+ {
+ name += " - SAT";
+ }
+ else if (string.Equals(info.lineupType, "ota", StringComparison.OrdinalIgnoreCase))
+ {
+ name += " - OTA";
+ }
+
+ return name;
+ }
+
+ private async Task<T> GetResponse<T>(string url, Func<string, string> filter = null)
+ where T : class
+ {
+ using (var stream = await _httpClient.Get(new HttpRequestOptions
+ {
+ Url = url,
+ CacheLength = TimeSpan.FromDays(1),
+ CacheMode = CacheMode.Unconditional
+
+ }).ConfigureAwait(false))
+ {
+ using (var reader = new StreamReader(stream))
+ {
+ var path = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+ using (var secondStream = await _httpClient.Get(new HttpRequestOptions
+ {
+ Url = "https://www.mb3admin.com" + path,
+ CacheLength = TimeSpan.FromDays(1),
+ CacheMode = CacheMode.Unconditional
+
+ }).ConfigureAwait(false))
+ {
+ return ParseResponse<T>(secondStream, filter);
+ }
+ }
+ }
+ }
+
+ private T ParseResponse<T>(Stream response, Func<string,string> filter)
+ {
+ using (var reader = new StreamReader(response))
+ {
+ var json = reader.ReadToEnd();
+
+ if (filter != null)
+ {
+ json = filter(json);
+ }
+
+ return _jsonSerializer.DeserializeFromString<T>(json);
+ }
+ }
+
+ private class LineupInfo
+ {
+ public string lineupID { get; set; }
+ public string lineupName { get; set; }
+ public string lineupType { get; set; }
+ public string providerID { get; set; }
+ public string providerName { get; set; }
+ public string serviceArea { get; set; }
+ public string country { get; set; }
+ }
+
+ private class Station
+ {
+ public string number { get; set; }
+ public int channelNumber { get; set; }
+ public int subChannelNumber { get; set; }
+ public int stationID { get; set; }
+ public string name { get; set; }
+ public string callsign { get; set; }
+ public string network { get; set; }
+ public string stationType { get; set; }
+ public int NTSC_TSID { get; set; }
+ public int DTV_TSID { get; set; }
+ public string webLink { get; set; }
+ public string logoFilename { get; set; }
+ }
+
+ private class LineupDetailResponse
+ {
+ public string lineupID { get; set; }
+ public string lineupName { get; set; }
+ public string lineupType { get; set; }
+ public string providerID { get; set; }
+ public string providerName { get; set; }
+ public string serviceArea { get; set; }
+ public string country { get; set; }
+ public List<Station> stations { get; set; }
+ }
+
+ private class ListingInfo
+ {
+ public string number { get; set; }
+ public int channelNumber { get; set; }
+ public int subChannelNumber { get; set; }
+ public int stationID { get; set; }
+ public string name { get; set; }
+ public string callsign { get; set; }
+ public string network { get; set; }
+ public string stationType { get; set; }
+ public string webLink { get; set; }
+ public string logoFilename { get; set; }
+ public int listingID { get; set; }
+ public string listDateTime { get; set; }
+ public int duration { get; set; }
+ public int showID { get; set; }
+ public int seriesID { get; set; }
+ public string showName { get; set; }
+ public string episodeTitle { get; set; }
+ public string episodeNumber { get; set; }
+ public int parts { get; set; }
+ public int partNum { get; set; }
+ public bool seriesPremiere { get; set; }
+ public bool seasonPremiere { get; set; }
+ public bool seriesFinale { get; set; }
+ public bool seasonFinale { get; set; }
+ public bool repeat { get; set; }
+ public bool @new { get; set; }
+ public string rating { get; set; }
+ public bool captioned { get; set; }
+ public bool educational { get; set; }
+ public bool blackWhite { get; set; }
+ public bool subtitled { get; set; }
+ public bool live { get; set; }
+ public bool hd { get; set; }
+ public bool descriptiveVideo { get; set; }
+ public bool inProgress { get; set; }
+ public string showTypeID { get; set; }
+ public int breakoutLevel { get; set; }
+ public string showType { get; set; }
+ public string year { get; set; }
+ public string guest { get; set; }
+ public string cast { get; set; }
+ public string director { get; set; }
+ public int starRating { get; set; }
+ public string description { get; set; }
+ public string league { get; set; }
+ public string team1 { get; set; }
+ public string team2 { get; set; }
+ public string @event { get; set; }
+ public string location { get; set; }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/IEmbyListingProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/IEmbyListingProvider.cs
new file mode 100644
index 000000000..95c22b986
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/IEmbyListingProvider.cs
@@ -0,0 +1,18 @@
+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.Emby
+{
+ public interface IEmbyListingProvider
+ {
+ Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken);
+ Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken);
+ Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings);
+ Task<List<NameIdPair>> GetLineups(string country, string location);
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
new file mode 100644
index 000000000..d53c08150
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -0,0 +1,982 @@
+using System.Net;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.Listings
+{
+ public class SchedulesDirect : IListingsProvider
+ {
+ private readonly ILogger _logger;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IHttpClient _httpClient;
+ private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
+ private readonly IApplicationHost _appHost;
+
+ private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
+
+ private readonly ConcurrentDictionary<string, ScheduleDirect.Station> _channelPair =
+ new ConcurrentDictionary<string, ScheduleDirect.Station>();
+
+ public SchedulesDirect(ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IApplicationHost appHost)
+ {
+ _logger = logger;
+ _jsonSerializer = jsonSerializer;
+ _httpClient = httpClient;
+ _appHost = appHost;
+ }
+
+ private string UserAgent
+ {
+ get { return "Emby/" + _appHost.ApplicationVersion; }
+ }
+
+ private List<string> GetScheduleRequestDates(DateTime startDateUtc, DateTime endDateUtc)
+ {
+ List<string> dates = new List<string>();
+
+ var start = new List<DateTime> { startDateUtc, startDateUtc.ToLocalTime() }.Min();
+ var end = new List<DateTime> { endDateUtc, endDateUtc.ToLocalTime() }.Max();
+
+ while (start.DayOfYear <= end.Day)
+ {
+ dates.Add(start.ToString("yyyy-MM-dd"));
+ start = start.AddDays(1);
+ }
+
+ return dates;
+ }
+
+ public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+ {
+ List<ProgramInfo> programsInfo = new List<ProgramInfo>();
+
+ var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ return programsInfo;
+ }
+
+ if (string.IsNullOrWhiteSpace(info.ListingsId))
+ {
+ return programsInfo;
+ }
+
+ var httpOptions = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/schedules",
+ UserAgent = UserAgent,
+ CancellationToken = cancellationToken,
+ // The data can be large so give it some extra time
+ TimeoutMs = 60000
+ };
+
+ httpOptions.RequestHeaders["token"] = token;
+
+ var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
+
+ ScheduleDirect.Station station = null;
+
+ if (!_channelPair.TryGetValue(channelNumber, out station))
+ {
+ return programsInfo;
+ }
+ string stationID = station.stationID;
+
+ _logger.Info("Channel Station ID is: " + stationID);
+ List<ScheduleDirect.RequestScheduleForChannel> requestList =
+ new List<ScheduleDirect.RequestScheduleForChannel>()
+ {
+ new ScheduleDirect.RequestScheduleForChannel()
+ {
+ stationID = stationID,
+ date = dates
+ }
+ };
+
+ var requestString = _jsonSerializer.SerializeToString(requestList);
+ _logger.Debug("Request string for schedules is: " + requestString);
+ httpOptions.RequestContent = requestString;
+ using (var response = await _httpClient.Post(httpOptions))
+ {
+ StreamReader reader = new StreamReader(response.Content);
+ string responseString = reader.ReadToEnd();
+ var dailySchedules = _jsonSerializer.DeserializeFromString<List<ScheduleDirect.Day>>(responseString);
+ _logger.Debug("Found " + dailySchedules.Count() + " programs on " + channelNumber + " ScheduleDirect");
+
+ httpOptions = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/programs",
+ UserAgent = UserAgent,
+ CancellationToken = cancellationToken
+ };
+
+ httpOptions.RequestHeaders["token"] = token;
+
+ List<string> programsID = new List<string>();
+ programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct().ToList();
+ var requestBody = "[\"" + string.Join("\", \"", programsID) + "\"]";
+ httpOptions.RequestContent = requestBody;
+
+ using (var innerResponse = await _httpClient.Post(httpOptions))
+ {
+ StreamReader innerReader = new StreamReader(innerResponse.Content);
+ responseString = innerReader.ReadToEnd();
+
+ var programDetails =
+ _jsonSerializer.DeserializeFromString<List<ScheduleDirect.ProgramDetails>>(
+ responseString);
+ var programDict = programDetails.ToDictionary(p => p.programID, y => y);
+
+ var images = await GetImageForPrograms(programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID).ToList(), cancellationToken);
+
+ var schedules = dailySchedules.SelectMany(d => d.programs);
+ foreach (ScheduleDirect.Program schedule in schedules)
+ {
+ //_logger.Debug("Proccesing Schedule for statio ID " + stationID +
+ // " which corresponds to channel " + channelNumber + " and program id " +
+ // schedule.programID + " which says it has images? " +
+ // programDict[schedule.programID].hasImageArtwork);
+
+ var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
+ if (imageIndex > -1)
+ {
+ programDict[schedule.programID].images = GetProgramLogo(ApiUrl, images[imageIndex]);
+ }
+
+ programsInfo.Add(GetProgram(channelNumber, schedule, programDict[schedule.programID]));
+ }
+ _logger.Info("Finished with EPGData");
+ }
+ }
+
+ return programsInfo;
+ }
+
+ public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(info.ListingsId))
+ {
+ throw new Exception("ListingsId required");
+ }
+
+ var token = await GetToken(info, cancellationToken);
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ throw new Exception("token required");
+ }
+
+ _channelPair.Clear();
+
+ var httpOptions = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/lineups/" + info.ListingsId,
+ UserAgent = UserAgent,
+ CancellationToken = cancellationToken
+ };
+
+ httpOptions.RequestHeaders["token"] = token;
+
+ using (var response = await _httpClient.Get(httpOptions))
+ {
+ 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 channel = (map.channel ?? (map.atscMajor + "." + map.atscMinor)).TrimStart('0');
+ _logger.Debug("Found channel: " + channel + " in Schedules Direct");
+ var schChannel = root.stations.FirstOrDefault(item => item.stationID == map.stationID);
+
+ if (!_channelPair.ContainsKey(channel) && channel != "0.0" && schChannel != null)
+ {
+ _channelPair.TryAdd(channel, schChannel);
+ }
+ }
+ _logger.Info("Added " + _channelPair.Count() + " channels to the dictionary");
+
+ foreach (ChannelInfo channel in channels)
+ {
+ // Helper.logger.Info("Modifyin channel " + channel.Number);
+ if (_channelPair.ContainsKey(channel.Number))
+ {
+ string channelName;
+ if (_channelPair[channel.Number].logo != null)
+ {
+ channel.ImageUrl = _channelPair[channel.Number].logo.URL;
+ channel.HasImage = true;
+ }
+ if (_channelPair[channel.Number].affiliate != null)
+ {
+ channelName = _channelPair[channel.Number].affiliate;
+ }
+ else
+ {
+ channelName = _channelPair[channel.Number].name;
+ }
+ channel.Name = channelName;
+ }
+ else
+ {
+ _logger.Info("Schedules Direct doesnt have data for channel: " + channel.Number + " " +
+ channel.Name);
+ }
+ }
+ }
+ }
+
+ private ProgramInfo GetProgram(string channel, ScheduleDirect.Program programInfo,
+ ScheduleDirect.ProgramDetails details)
+ {
+ //_logger.Debug("Show type is: " + (details.showType ?? "No ShowType"));
+ DateTime startAt = DateTime.ParseExact(programInfo.airDateTime, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'",
+ CultureInfo.InvariantCulture);
+ DateTime endAt = startAt.AddSeconds(programInfo.duration);
+ ProgramAudio audioType = ProgramAudio.Stereo;
+
+ bool repeat = (programInfo.@new == null);
+ string newID = programInfo.programID + "T" + startAt.Ticks + "C" + channel;
+
+ if (programInfo.audioProperties != null)
+ {
+ if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
+ {
+ audioType = ProgramAudio.DolbyDigital;
+ }
+ else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
+ {
+ audioType = ProgramAudio.DolbyDigital;
+ }
+ else if (programInfo.audioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
+ {
+ audioType = ProgramAudio.Stereo;
+ }
+ else
+ {
+ audioType = ProgramAudio.Mono;
+ }
+ }
+
+ string episodeTitle = null;
+ if (details.episodeTitle150 != null)
+ {
+ episodeTitle = details.episodeTitle150;
+ }
+
+ string imageUrl = null;
+
+ if (details.hasImageArtwork)
+ {
+ imageUrl = details.images;
+ }
+
+ var showType = details.showType ?? string.Empty;
+
+ var info = new ProgramInfo
+ {
+ ChannelId = channel,
+ Id = newID,
+ StartDate = startAt,
+ EndDate = endAt,
+ Name = details.titles[0].title120 ?? "Unkown",
+ OfficialRating = null,
+ CommunityRating = null,
+ EpisodeTitle = episodeTitle,
+ Audio = audioType,
+ IsRepeat = repeat,
+ IsSeries = showType.IndexOf("series", StringComparison.OrdinalIgnoreCase) != -1,
+ ImageUrl = imageUrl,
+ HasImage = details.hasImageArtwork,
+ IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase),
+ IsSports = showType.IndexOf("sports", StringComparison.OrdinalIgnoreCase) != -1,
+ IsMovie = showType.IndexOf("movie", StringComparison.OrdinalIgnoreCase) != -1 || showType.IndexOf("film", StringComparison.OrdinalIgnoreCase) != -1,
+ ShowId = programInfo.programID,
+ Etag = programInfo.md5
+ };
+
+ if (programInfo.videoProperties != null)
+ {
+ info.IsHD = programInfo.videoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (details.contentRating != null && details.contentRating.Count > 0)
+ {
+ info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-").Replace("--", "-");
+
+ var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" };
+ if (invalid.Contains(info.OfficialRating, StringComparer.OrdinalIgnoreCase))
+ {
+ info.OfficialRating = null;
+ }
+ }
+
+ if (details.descriptions != null)
+ {
+ if (details.descriptions.description1000 != null)
+ {
+ info.Overview = details.descriptions.description1000[0].description;
+ }
+ else if (details.descriptions.description100 != null)
+ {
+ info.ShortOverview = details.descriptions.description100[0].description;
+ }
+ }
+
+ if (info.IsSeries)
+ {
+ info.SeriesId = programInfo.programID.Substring(0, 10);
+
+ if (details.metadata != null)
+ {
+ var gracenote = details.metadata.Find(x => x.Gracenote != null).Gracenote;
+ info.SeasonNumber = gracenote.season;
+ info.EpisodeNumber = gracenote.episode;
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(details.originalAirDate))
+ {
+ info.OriginalAirDate = DateTime.Parse(details.originalAirDate);
+ }
+
+ if (details.genres != null)
+ {
+ info.Genres = details.genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
+ info.IsNews = details.genres.Contains("news", StringComparer.OrdinalIgnoreCase);
+
+ if (info.Genres.Contains("children", StringComparer.OrdinalIgnoreCase))
+ {
+ info.IsKids = true;
+ }
+ }
+
+ return info;
+ }
+
+ private string GetProgramLogo(string apiUrl, ScheduleDirect.ShowImages images)
+ {
+ string url = "";
+ if (images.data != null)
+ {
+ var smallImages = images.data.Where(i => i.size == "Sm").ToList();
+ if (smallImages.Any())
+ {
+ images.data = smallImages;
+ }
+ var logoIndex = images.data.FindIndex(i => i.category == "Logo");
+ if (logoIndex == -1)
+ {
+ logoIndex = 0;
+ }
+ if (images.data[logoIndex].uri.Contains("http"))
+ {
+ url = images.data[logoIndex].uri;
+ }
+ else
+ {
+ url = apiUrl + "/image/" + images.data[logoIndex].uri;
+ }
+ //_logger.Debug("URL for image is : " + url);
+ }
+ return url;
+ }
+
+ private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(List<string> programIds,
+ CancellationToken cancellationToken)
+ {
+ var imageIdString = "[";
+
+ programIds.ForEach(i =>
+ {
+ if (!imageIdString.Contains(i.Substring(0, 10)))
+ {
+ imageIdString += "\"" + i.Substring(0, 10) + "\",";
+ }
+ ;
+ });
+ imageIdString = imageIdString.TrimEnd(',') + "]";
+
+ var httpOptions = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/metadata/programs",
+ UserAgent = UserAgent,
+ CancellationToken = cancellationToken,
+ RequestContent = imageIdString
+ };
+ List<ScheduleDirect.ShowImages> images;
+ using (var innerResponse2 = await _httpClient.Post(httpOptions))
+ {
+ images = _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.ShowImages>>(
+ innerResponse2.Content);
+ }
+
+ return images;
+ }
+
+ public async Task<List<NameIdPair>> GetHeadends(ListingsProviderInfo info, string country, string location, CancellationToken cancellationToken)
+ {
+ var token = await GetToken(info, cancellationToken);
+
+ var lineups = new List<NameIdPair>();
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ return lineups;
+ }
+
+ var options = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location,
+ UserAgent = UserAgent,
+ CancellationToken = cancellationToken
+ };
+
+ options.RequestHeaders["token"] = token;
+
+ try
+ {
+ using (Stream responce = await _httpClient.Get(options).ConfigureAwait(false))
+ {
+ var root = _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.Headends>>(responce);
+
+ if (root != null)
+ {
+ foreach (ScheduleDirect.Headends headend in root)
+ {
+ foreach (ScheduleDirect.Lineup lineup in headend.lineups)
+ {
+ lineups.Add(new NameIdPair
+ {
+ Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
+ Id = lineup.uri.Substring(18)
+ });
+ }
+ }
+ }
+ else
+ {
+ _logger.Info("No lineups available");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Error getting headends", ex);
+ }
+
+ return lineups;
+ }
+
+ private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
+ private DateTime _lastErrorResponse;
+ private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken)
+ {
+ var username = info.Username;
+
+ // Reset the token if there's no username
+ if (string.IsNullOrWhiteSpace(username))
+ {
+ return null;
+ }
+
+ var password = info.Password;
+ if (string.IsNullOrWhiteSpace(password))
+ {
+ return null;
+ }
+
+ // Avoid hammering SD
+ if ((DateTime.UtcNow - _lastErrorResponse).TotalMinutes < 1)
+ {
+ return null;
+ }
+
+ NameValuePair savedToken = null;
+ if (!_tokens.TryGetValue(username, out savedToken))
+ {
+ savedToken = new NameValuePair();
+ _tokens.TryAdd(username, savedToken);
+ }
+
+ if (!string.IsNullOrWhiteSpace(savedToken.Name) && !string.IsNullOrWhiteSpace(savedToken.Value))
+ {
+ long ticks;
+ if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out ticks))
+ {
+ // If it's under 24 hours old we can still use it
+ if ((DateTime.UtcNow.Ticks - ticks) < TimeSpan.FromHours(24).Ticks)
+ {
+ return savedToken.Name;
+ }
+ }
+ }
+
+ await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
+ savedToken.Name = result;
+ savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
+ return result;
+ }
+ catch (HttpException ex)
+ {
+ if (ex.StatusCode.HasValue)
+ {
+ if ((int)ex.StatusCode.Value == 400)
+ {
+ _tokens.Clear();
+ _lastErrorResponse = DateTime.UtcNow;
+ }
+ }
+ throw;
+ }
+ finally
+ {
+ _tokenSemaphore.Release();
+ }
+ }
+
+ private async Task<string> GetTokenInternal(string username, string password,
+ CancellationToken cancellationToken)
+ {
+ var httpOptions = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/token",
+ UserAgent = UserAgent,
+ RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}",
+ CancellationToken = cancellationToken
+ };
+ //_logger.Info("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " +
+ // httpOptions.RequestContent);
+
+ using (var responce = await _httpClient.Post(httpOptions))
+ {
+ var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Token>(responce.Content);
+ if (root.message == "OK")
+ {
+ _logger.Info("Authenticated with Schedules Direct token: " + root.token);
+ return root.token;
+ }
+
+ throw new ApplicationException("Could not authenticate with Schedules Direct Error: " + root.message);
+ }
+ }
+
+ private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
+ {
+ var token = await GetToken(info, cancellationToken);
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ throw new ArgumentException("Authentication required.");
+ }
+
+ if (string.IsNullOrWhiteSpace(info.ListingsId))
+ {
+ throw new ArgumentException("Listings Id required");
+ }
+
+ _logger.Info("Adding new LineUp ");
+
+ var httpOptions = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/lineups/" + info.ListingsId,
+ UserAgent = UserAgent,
+ CancellationToken = cancellationToken
+ };
+
+ httpOptions.RequestHeaders["token"] = token;
+
+ using (var response = await _httpClient.SendAsync(httpOptions, "PUT"))
+ {
+ }
+ }
+
+ public string Name
+ {
+ get { return "Schedules Direct"; }
+ }
+
+ public string Type
+ {
+ get { return "SchedulesDirect"; }
+ }
+
+ private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(info.ListingsId))
+ {
+ throw new ArgumentException("Listings Id required");
+ }
+
+ var token = await GetToken(info, cancellationToken);
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ throw new Exception("token required");
+ }
+
+ _logger.Info("Headends on account ");
+
+ var options = new HttpRequestOptions()
+ {
+ Url = ApiUrl + "/lineups",
+ UserAgent = UserAgent,
+ CancellationToken = cancellationToken
+ };
+
+ options.RequestHeaders["token"] = token;
+
+ try
+ {
+ using (var response = await _httpClient.Get(options).ConfigureAwait(false))
+ {
+ var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Lineups>(response);
+
+ return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+ catch (HttpException ex)
+ {
+ // Apparently we're supposed to swallow this
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ {
+ return false;
+ }
+
+ throw;
+ }
+ }
+
+ public async Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
+ {
+ if (validateLogin)
+ {
+ if (string.IsNullOrWhiteSpace(info.Username))
+ {
+ throw new ArgumentException("Username is required");
+ }
+ if (string.IsNullOrWhiteSpace(info.Password))
+ {
+ throw new ArgumentException("Password is required");
+ }
+ }
+ if (validateListings)
+ {
+ if (string.IsNullOrWhiteSpace(info.ListingsId))
+ {
+ throw new ArgumentException("Listings Id required");
+ }
+
+ var hasLineup = await HasLineup(info, CancellationToken.None).ConfigureAwait(false);
+
+ if (!hasLineup)
+ {
+ await AddLineupToAccount(info, CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+ }
+
+ public Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
+ {
+ return GetHeadends(info, country, location, CancellationToken.None);
+ }
+
+ public class ScheduleDirect
+ {
+ public class Token
+ {
+ public int code { get; set; }
+ public string message { get; set; }
+ public string serverID { get; set; }
+ public string token { get; set; }
+ }
+ public class Lineup
+ {
+ public string lineup { get; set; }
+ public string name { get; set; }
+ public string transport { get; set; }
+ public string location { get; set; }
+ public string uri { get; set; }
+ }
+
+ public class Lineups
+ {
+ public int code { get; set; }
+ public string serverID { get; set; }
+ public string datetime { get; set; }
+ public List<Lineup> lineups { get; set; }
+ }
+
+
+ public class Headends
+ {
+ public string headend { get; set; }
+ public string transport { get; set; }
+ public string location { get; set; }
+ public List<Lineup> lineups { get; set; }
+ }
+
+
+
+ public class Map
+ {
+ public string stationID { get; set; }
+ public string channel { get; set; }
+ public int uhfVhf { get; set; }
+ public int atscMajor { get; set; }
+ public int atscMinor { get; set; }
+ }
+
+ public class Broadcaster
+ {
+ public string city { get; set; }
+ public string state { get; set; }
+ public string postalcode { get; set; }
+ public string country { get; set; }
+ }
+
+ public class Logo
+ {
+ public string URL { get; set; }
+ public int height { get; set; }
+ public int width { get; set; }
+ public string md5 { get; set; }
+ }
+
+ public class Station
+ {
+ public string stationID { get; set; }
+ public string name { get; set; }
+ public string callsign { get; set; }
+ public List<string> broadcastLanguage { get; set; }
+ public List<string> descriptionLanguage { get; set; }
+ public Broadcaster broadcaster { get; set; }
+ public string affiliate { get; set; }
+ public Logo logo { get; set; }
+ public bool? isCommercialFree { get; set; }
+ }
+
+ public class Metadata
+ {
+ public string lineup { get; set; }
+ public string modified { get; set; }
+ public string transport { get; set; }
+ }
+
+ public class Channel
+ {
+ public List<Map> map { get; set; }
+ public List<Station> stations { get; set; }
+ public Metadata metadata { get; set; }
+ }
+
+ public class RequestScheduleForChannel
+ {
+ public string stationID { get; set; }
+ public List<string> date { get; set; }
+ }
+
+
+
+
+ public class Rating
+ {
+ public string body { get; set; }
+ public string code { get; set; }
+ }
+
+ public class Multipart
+ {
+ public int partNumber { get; set; }
+ public int totalParts { get; set; }
+ }
+
+ public class Program
+ {
+ public string programID { get; set; }
+ public string airDateTime { get; set; }
+ public int duration { get; set; }
+ public string md5 { get; set; }
+ public List<string> audioProperties { get; set; }
+ public List<string> videoProperties { get; set; }
+ public List<Rating> ratings { get; set; }
+ public bool? @new { get; set; }
+ public Multipart multipart { get; set; }
+ }
+
+
+
+ public class MetadataSchedule
+ {
+ public string modified { get; set; }
+ public string md5 { get; set; }
+ public string startDate { get; set; }
+ public string endDate { get; set; }
+ public int days { get; set; }
+ }
+
+ public class Day
+ {
+ public string stationID { get; set; }
+ public List<Program> programs { get; set; }
+ public MetadataSchedule metadata { get; set; }
+ }
+
+ //
+ public class Title
+ {
+ public string title120 { get; set; }
+ }
+
+ public class EventDetails
+ {
+ public string subType { get; set; }
+ }
+
+ public class Description100
+ {
+ public string descriptionLanguage { get; set; }
+ public string description { get; set; }
+ }
+
+ public class Description1000
+ {
+ public string descriptionLanguage { get; set; }
+ public string description { get; set; }
+ }
+
+ public class DescriptionsProgram
+ {
+ public List<Description100> description100 { get; set; }
+ public List<Description1000> description1000 { get; set; }
+ }
+
+ public class Gracenote
+ {
+ public int season { get; set; }
+ public int episode { get; set; }
+ }
+
+ public class MetadataPrograms
+ {
+ public Gracenote Gracenote { get; set; }
+ }
+
+ public class ContentRating
+ {
+ public string body { get; set; }
+ public string code { get; set; }
+ }
+
+ public class Cast
+ {
+ public string billingOrder { get; set; }
+ public string role { get; set; }
+ public string nameId { get; set; }
+ public string personId { get; set; }
+ public string name { get; set; }
+ public string characterName { get; set; }
+ }
+
+ public class Crew
+ {
+ public string billingOrder { get; set; }
+ public string role { get; set; }
+ public string nameId { get; set; }
+ public string personId { get; set; }
+ public string name { get; set; }
+ }
+
+ public class QualityRating
+ {
+ public string ratingsBody { get; set; }
+ public string rating { get; set; }
+ public string minRating { get; set; }
+ public string maxRating { get; set; }
+ public string increment { get; set; }
+ }
+
+ public class Movie
+ {
+ public string year { get; set; }
+ public int duration { get; set; }
+ public List<QualityRating> qualityRating { get; set; }
+ }
+
+ public class Recommendation
+ {
+ public string programID { get; set; }
+ public string title120 { get; set; }
+ }
+
+ public class ProgramDetails
+ {
+ public string audience { get; set; }
+ public string programID { get; set; }
+ public List<Title> titles { get; set; }
+ public EventDetails eventDetails { get; set; }
+ public DescriptionsProgram descriptions { get; set; }
+ public string originalAirDate { get; set; }
+ public List<string> genres { get; set; }
+ public string episodeTitle150 { get; set; }
+ public List<MetadataPrograms> metadata { get; set; }
+ public List<ContentRating> contentRating { get; set; }
+ public List<Cast> cast { get; set; }
+ public List<Crew> crew { get; set; }
+ public string showType { get; set; }
+ public bool hasImageArtwork { get; set; }
+ public string images { get; set; }
+ public string imageID { get; set; }
+ public string md5 { get; set; }
+ public List<string> contentAdvisory { get; set; }
+ public Movie movie { get; set; }
+ public List<Recommendation> recommendations { get; set; }
+ }
+
+ public class Caption
+ {
+ public string content { get; set; }
+ public string lang { get; set; }
+ }
+
+ public class ImageData
+ {
+ public string width { get; set; }
+ public string height { get; set; }
+ public string uri { get; set; }
+ public string size { get; set; }
+ public string aspect { get; set; }
+ public string category { get; set; }
+ public string text { get; set; }
+ public string primary { get; set; }
+ public string tier { get; set; }
+ public Caption caption { get; set; }
+ }
+
+ public class ShowImages
+ {
+ public string programID { get; set; }
+ public List<ImageData> data { get; set; }
+ }
+
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs
new file mode 100644
index 000000000..de107ced8
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs
@@ -0,0 +1,44 @@
+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, 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/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 72fea2c79..9ffd8a500 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -198,10 +198,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
/// Gets the channel info dto.
/// </summary>
/// <param name="info">The info.</param>
+ /// <param name="options">The options.</param>
/// <param name="currentProgram">The current program.</param>
/// <param name="user">The user.</param>
/// <returns>ChannelInfoDto.</returns>
- public ChannelInfoDto GetChannelInfoDto(LiveTvChannel info, LiveTvProgram currentProgram, User user = null)
+ public ChannelInfoDto GetChannelInfoDto(LiveTvChannel info, DtoOptions options, LiveTvProgram currentProgram, User user = null)
{
var dto = new ChannelInfoDto
{
@@ -238,7 +239,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
if (currentProgram != null)
{
- dto.CurrentProgram = _dtoService.GetBaseItemDto(currentProgram, new DtoOptions(), user);
+ dto.CurrentProgram = _dtoService.GetBaseItemDto(currentProgram, options, user);
}
return dto;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
index f6c69d8d6..d73b144b8 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1,6 +1,7 @@
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
using MediaBrowser.Common.Progress;
using MediaBrowser.Common.ScheduledTasks;
using MediaBrowser.Controller.Configuration;
@@ -13,15 +14,18 @@ using MediaBrowser.Controller.Localization;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
+using MoreLinq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -55,7 +59,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1);
- 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)
+ private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>();
+ private readonly List<IListingsProvider> _listingProviders = new List<IListingsProvider>();
+ private readonly IFileSystem _fileSystem;
+
+ 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;
_logger = logger;
@@ -66,6 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
_localization = localization;
_jsonSerializer = jsonSerializer;
_providerManager = providerManager;
+ _fileSystem = fileSystem;
_dtoService = dtoService;
_userDataManager = userDataManager;
@@ -90,9 +99,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
/// Adds the parts.
/// </summary>
/// <param name="services">The services.</param>
- public void AddParts(IEnumerable<ILiveTvService> services)
+ /// <param name="tunerHosts">The tuner hosts.</param>
+ /// <param name="listingProviders">The listing providers.</param>
+ public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders)
{
_services.AddRange(services);
+ _tunerHosts.AddRange(tunerHosts);
+ _listingProviders.AddRange(listingProviders);
foreach (var service in _services)
{
@@ -100,6 +113,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
+ public List<ITunerHost> TunerHosts
+ {
+ get { return _tunerHosts; }
+ }
+
+ public List<IListingsProvider> ListingProviders
+ {
+ get { return _listingProviders; }
+ }
+
void service_DataSourceChanged(object sender, EventArgs e)
{
_taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
@@ -218,7 +241,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return result;
}
- public async Task<QueryResult<ChannelInfoDto>> GetChannels(LiveTvChannelQuery query, CancellationToken cancellationToken)
+ public async Task<QueryResult<ChannelInfoDto>> GetChannels(LiveTvChannelQuery query, DtoOptions options, CancellationToken cancellationToken)
{
var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId);
@@ -228,20 +251,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var now = DateTime.UtcNow;
- var programs = _libraryManager.GetItems(new InternalItemsQuery
+ var programs = query.AddCurrentProgram ? _libraryManager.QueryItems(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
MaxStartDate = now,
- MinEndDate = now
+ MinEndDate = now,
+ ChannelIds = internalResult.Items.Select(i => i.Id.ToString("N")).ToArray()
- }).Items.Cast<LiveTvProgram>().OrderBy(i => i.StartDate).ToList();
+ }).Items.Cast<LiveTvProgram>().OrderBy(i => i.StartDate).ToList() : new List<LiveTvProgram>();
foreach (var channel in internalResult.Items)
{
var channelIdString = channel.Id.ToString("N");
var currentProgram = programs.FirstOrDefault(i => string.Equals(i.ChannelId, channelIdString, StringComparison.OrdinalIgnoreCase));
- returnList.Add(_tvDtoService.GetChannelInfoDto(channel, currentProgram, user));
+ returnList.Add(_tvDtoService.GetChannelInfoDto(channel, options, currentProgram, user));
}
var result = new QueryResult<ChannelInfoDto>
@@ -310,6 +334,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var service = GetService(item);
var sources = await service.GetChannelStreamMediaSources(item.ExternalId, cancellationToken).ConfigureAwait(false);
+
+ if (sources.Count == 0)
+ {
+ throw new NotImplementedException();
+ }
+
var list = sources.ToList();
foreach (var source in list)
@@ -334,6 +364,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+ if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
+ {
+ mediaSourceId = null;
+ }
+
try
{
MediaSourceInfo info;
@@ -525,16 +560,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var replaceImages = new List<ImageType>();
- if (!string.Equals(item.ProviderImageUrl, channelInfo.ImageUrl, StringComparison.OrdinalIgnoreCase))
- {
- isNew = true;
- replaceImages.Add(ImageType.Primary);
- }
- if (!string.Equals(item.ProviderImagePath, channelInfo.ImagePath, StringComparison.OrdinalIgnoreCase))
- {
- isNew = true;
- replaceImages.Add(ImageType.Primary);
- }
+ //if (!string.Equals(item.ProviderImageUrl, channelInfo.ImageUrl, StringComparison.OrdinalIgnoreCase))
+ //{
+ // isNew = true;
+ // replaceImages.Add(ImageType.Primary);
+ //}
+ //if (!string.Equals(item.ProviderImagePath, channelInfo.ImagePath, StringComparison.OrdinalIgnoreCase))
+ //{
+ // isNew = true;
+ // replaceImages.Add(ImageType.Primary);
+ //}
item.ProviderImageUrl = channelInfo.ImageUrl;
item.HasProviderImage = channelInfo.HasImage;
@@ -570,7 +605,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
Name = info.Name,
Id = id,
DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow
+ DateModified = DateTime.UtcNow,
+ Etag = info.Etag
};
}
@@ -602,17 +638,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv
item.ProviderImageUrl = info.ImageUrl;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
item.StartDate = info.StartDate;
+ item.HomePageUrl = info.HomePageUrl;
item.ProductionYear = info.ProductionYear;
item.PremiereDate = item.PremiereDate ?? info.OriginalAirDate;
+ item.IndexNumber = info.EpisodeNumber;
+ item.ParentIndexNumber = info.SeasonNumber;
+
if (isNew)
{
await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
}
else
{
- await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ if (string.IsNullOrWhiteSpace(info.Etag) || !string.Equals(info.Etag, item.Etag, StringComparison.OrdinalIgnoreCase))
+ {
+ item.Etag = info.Etag;
+ await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ }
}
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions());
@@ -694,6 +738,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
if (!string.IsNullOrEmpty(info.Path))
{
item.Path = info.Path;
+ var fileInfo = new FileInfo(info.Path);
+
+ recording.DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo);
+ recording.DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo);
}
else if (!string.IsNullOrEmpty(info.Url))
{
@@ -706,7 +754,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
}
- else if (pathChanged)
+ else if (pathChanged || info.DateLastUpdated > recording.DateLastSaved)
{
await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
@@ -727,7 +775,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return dto;
}
- public async Task<QueryResult<BaseItemDto>> GetPrograms(ProgramQuery query, CancellationToken cancellationToken)
+ public async Task<QueryResult<BaseItemDto>> GetPrograms(ProgramQuery query, DtoOptions options, CancellationToken cancellationToken)
{
var internalQuery = new InternalItemsQuery
{
@@ -738,9 +786,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv
MaxStartDate = query.MaxStartDate,
ChannelIds = query.ChannelIds,
IsMovie = query.IsMovie,
- IsSports = query.IsSports
+ IsSports = query.IsSports,
+ IsKids = query.IsKids,
+ Genres = query.Genres,
+ StartIndex = query.StartIndex,
+ Limit = query.Limit,
+ SortBy = query.SortBy,
+ SortOrder = query.SortOrder ?? SortOrder.Ascending
};
+ var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId);
+ if (user != null)
+ {
+ internalQuery.MaxParentalRating = user.Policy.MaxParentalRating;
+
+ if (user.Policy.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram))
+ {
+ internalQuery.HasParentalRating = true;
+ }
+ }
+
if (query.HasAired.HasValue)
{
if (query.HasAired.Value)
@@ -753,40 +818,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
- IEnumerable<LiveTvProgram> programs = _libraryManager.GetItems(internalQuery).Items.Cast<LiveTvProgram>();
-
- // Apply genre filter
- if (query.Genres.Length > 0)
- {
- programs = programs.Where(p => p.Genres.Any(g => query.Genres.Contains(g, StringComparer.OrdinalIgnoreCase)));
- }
-
- var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId);
- if (user != null)
- {
- // Avoid implicitly captured closure
- var currentUser = user;
- programs = programs.Where(i => i.IsVisible(currentUser));
- }
-
- programs = _libraryManager.Sort(programs, user, query.SortBy, query.SortOrder ?? SortOrder.Ascending)
- .Cast<LiveTvProgram>();
-
- var programList = programs.ToList();
- IEnumerable<LiveTvProgram> returnPrograms = programList;
-
- if (query.StartIndex.HasValue)
- {
- returnPrograms = returnPrograms.Skip(query.StartIndex.Value);
- }
-
- if (query.Limit.HasValue)
- {
- returnPrograms = returnPrograms.Take(query.Limit.Value);
- }
+ var queryResult = _libraryManager.QueryItems(internalQuery);
- var returnArray = returnPrograms
- .Select(i => _dtoService.GetBaseItemDto(i, new DtoOptions(), user))
+ var returnArray = queryResult.Items
+ .Select(i => _dtoService.GetBaseItemDto(i, options, user))
.ToArray();
await AddRecordingInfo(returnArray, cancellationToken).ConfigureAwait(false);
@@ -794,7 +829,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var result = new QueryResult<BaseItemDto>
{
Items = returnArray,
- TotalRecordCount = programList.Count
+ TotalRecordCount = queryResult.TotalRecordCount
};
return result;
@@ -807,7 +842,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
IsAiring = query.IsAiring,
IsMovie = query.IsMovie,
- IsSports = query.IsSports
+ IsSports = query.IsSports,
+ IsKids = query.IsKids
};
if (query.HasAired.HasValue)
@@ -822,19 +858,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
}
- IEnumerable<LiveTvProgram> programs = _libraryManager.GetItems(internalQuery).Items.Cast<LiveTvProgram>();
-
var user = _userManager.GetUserById(query.UserId);
+ if (user != null)
+ {
+ internalQuery.MaxParentalRating = user.Policy.MaxParentalRating;
+
+ if (user.Policy.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram))
+ {
+ internalQuery.HasParentalRating = true;
+ }
+ }
- // Avoid implicitly captured closure
- var currentUser = user;
- programs = programs.Where(i => i.IsVisible(currentUser));
+ IEnumerable<LiveTvProgram> programs = _libraryManager.QueryItems(internalQuery).Items.Cast<LiveTvProgram>();
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);
programs = programList.OrderBy(i => i.HasImage(ImageType.Primary) ? 0 : 1)
@@ -843,8 +886,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
if (query.Limit.HasValue)
{
- programs = programs.Take(query.Limit.Value)
- .OrderBy(i => i.StartDate);
+ programs = programs.Take(query.Limit.Value);
}
programList = programs.ToList();
@@ -860,14 +902,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return result;
}
- public async Task<QueryResult<BaseItemDto>> GetRecommendedPrograms(RecommendedProgramQuery query, CancellationToken cancellationToken)
+ public async Task<QueryResult<BaseItemDto>> GetRecommendedPrograms(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken)
{
var internalResult = await GetRecommendedProgramsInternal(query, cancellationToken).ConfigureAwait(false);
var user = _userManager.GetUserById(query.UserId);
var returnArray = internalResult.Items
- .Select(i => _dtoService.GetBaseItemDto(i, new DtoOptions(), user))
+ .Select(i => _dtoService.GetBaseItemDto(i, options, user))
.ToArray();
await AddRecordingInfo(returnArray, cancellationToken).ConfigureAwait(false);
@@ -1009,6 +1051,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
cancellationToken.ThrowIfCancellationRequested();
+ _logger.Debug("Refreshing guide from {0}", service.Name);
+
try
{
var innerProgress = new ActionableProgress<double>();
@@ -1340,21 +1384,45 @@ namespace MediaBrowser.Server.Implementations.LiveTv
dto.Id = _tvDtoService.GetInternalProgramId(service.Name, program.ExternalId).ToString("N");
- dto.ChannelId = item.ChannelId;
-
dto.StartDate = program.StartDate;
dto.IsRepeat = program.IsRepeat;
dto.EpisodeTitle = program.EpisodeTitle;
dto.ChannelType = program.ChannelType;
dto.Audio = program.Audio;
- dto.IsHD = program.IsHD;
- dto.IsMovie = program.IsMovie;
- dto.IsSeries = program.IsSeries;
- dto.IsSports = program.IsSports;
- dto.IsLive = program.IsLive;
- dto.IsNews = program.IsNews;
- dto.IsKids = program.IsKids;
- dto.IsPremiere = program.IsPremiere;
+
+ if (program.IsHD.HasValue && program.IsHD.Value)
+ {
+ dto.IsHD = program.IsHD;
+ }
+ if (program.IsMovie)
+ {
+ dto.IsMovie = program.IsMovie;
+ }
+ if (program.IsSeries)
+ {
+ dto.IsSeries = program.IsSeries;
+ }
+ if (program.IsSports)
+ {
+ dto.IsSports = program.IsSports;
+ }
+ if (program.IsLive)
+ {
+ dto.IsLive = program.IsLive;
+ }
+ if (program.IsNews)
+ {
+ dto.IsNews = program.IsNews;
+ }
+ if (program.IsKids)
+ {
+ dto.IsKids = program.IsKids;
+ }
+ if (program.IsPremiere)
+ {
+ dto.IsPremiere = program.IsPremiere;
+ }
+
dto.OriginalAirDate = program.OriginalAirDate;
if (channel != null)
@@ -1382,8 +1450,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
? null
: _tvDtoService.GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N");
- dto.ChannelId = item.ChannelId;
-
dto.StartDate = info.StartDate;
dto.RecordingStatus = info.Status;
dto.IsRepeat = info.IsRepeat;
@@ -1663,11 +1729,37 @@ namespace MediaBrowser.Server.Implementations.LiveTv
.OrderBy(i => i.StartDate)
.FirstOrDefault();
- var dto = _tvDtoService.GetChannelInfoDto(channel, currentProgram, user);
+ var dto = _tvDtoService.GetChannelInfoDto(channel, new DtoOptions(), currentProgram, user);
return dto;
}
+ public void AddChannelInfo(BaseItemDto dto, LiveTvChannel channel, DtoOptions options, User user)
+ {
+ dto.MediaSources = channel.GetMediaSources(true).ToList();
+
+ var now = DateTime.UtcNow;
+
+ var programs = _libraryManager.GetItems(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
+ ChannelIds = new[] { channel.Id.ToString("N") },
+ MaxStartDate = now,
+ MinEndDate = now,
+ Limit = 1
+
+ }).Items.Cast<LiveTvProgram>();
+
+ var currentProgram = programs
+ .OrderBy(i => i.StartDate)
+ .FirstOrDefault();
+
+ if (currentProgram != null)
+ {
+ dto.CurrentProgram = _dtoService.GetBaseItemDto(currentProgram, options, user);
+ }
+ }
+
private async Task<Tuple<SeriesTimerInfo, ILiveTvService>> GetNewTimerDefaultsInternal(CancellationToken cancellationToken, LiveTvProgram program = null)
{
var service = program != null && !string.IsNullOrWhiteSpace(program.ServiceName) ?
@@ -2029,6 +2121,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
info.Version = statusInfo.Version;
info.HasUpdateAvailable = statusInfo.HasUpdateAvailable;
info.HomePageUrl = service.HomePageUrl;
+ info.IsVisible = statusInfo.IsVisible;
info.Tuners = statusInfo.Tuners.Select(i =>
{
@@ -2081,7 +2174,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private bool IsLiveTvEnabled(User user)
{
- return user.Policy.EnableLiveTvAccess && Services.Count > 0;
+ return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Count(i => i.IsEnabled) > 0);
}
public IEnumerable<User> GetEnabledUsers()
@@ -2114,16 +2207,142 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
var user = string.IsNullOrEmpty(userId) ? null : _userManager.GetUserById(userId);
- var folder = await GetInternalLiveTvFolder(userId, cancellationToken).ConfigureAwait(false);
+ var folder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false);
return _dtoService.GetBaseItemDto(folder, new DtoOptions(), user);
}
- public async Task<Folder> GetInternalLiveTvFolder(string userId, CancellationToken cancellationToken)
+ public async Task<Folder> GetInternalLiveTvFolder(CancellationToken cancellationToken)
{
var name = _localization.GetLocalizedString("ViewTypeLiveTV");
- var user = _userManager.GetUserById(userId);
- return await _libraryManager.GetNamedView(user, name, "livetv", "zz_" + name, cancellationToken).ConfigureAwait(false);
+ return await _libraryManager.GetNamedView(name, "livetv", name, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info)
+ {
+ info = (TunerHostInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(TunerHostInfo));
+
+ var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+
+ if (provider == null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ await provider.Validate(info).ConfigureAwait(false);
+
+ var config = GetConfiguration();
+
+ var index = config.TunerHosts.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
+
+ if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
+ {
+ info.Id = Guid.NewGuid().ToString("N");
+ config.TunerHosts.Add(info);
+ }
+ else
+ {
+ config.TunerHosts[index] = info;
+ }
+
+ _config.SaveConfiguration("livetv", config);
+
+ _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+
+ return info;
+ }
+
+ public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
+ {
+ info = (ListingsProviderInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(ListingsProviderInfo));
+
+ var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+
+ if (provider == null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
+
+ var config = GetConfiguration();
+
+ var index = config.ListingProviders.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
+
+ if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
+ {
+ info.Id = Guid.NewGuid().ToString("N");
+ config.ListingProviders.Add(info);
+ }
+ else
+ {
+ config.ListingProviders[index] = info;
+ }
+
+ _config.SaveConfiguration("livetv", config);
+
+ _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+
+ return info;
+ }
+
+ public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location)
+ {
+ var config = GetConfiguration();
+
+ if (string.IsNullOrWhiteSpace(providerId))
+ {
+ var provider = _listingProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase));
+
+ if (provider == null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ return provider.GetLineups(null, country, location);
+ }
+ else
+ {
+ var info = config.ListingProviders.FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase));
+
+ var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+
+ if (provider == null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ return provider.GetLineups(info, country, location);
+ }
+ }
+
+ public Task<MBRegistrationRecord> GetRegistrationInfo(string channelId, string programId, string feature)
+ {
+ ILiveTvService service;
+
+ if (string.IsNullOrWhiteSpace(programId))
+ {
+ var channel = GetInternalChannel(channelId);
+ service = GetService(channel);
+ }
+ else
+ {
+ var program = GetInternalProgram(programId);
+ service = GetService(program);
+ }
+
+ var hasRegistration = service as IHasRegistrationInfo;
+
+ if (hasRegistration != null)
+ {
+ return hasRegistration.GetRegistrationInfo(feature);
+ }
+
+ return Task.FromResult(new MBRegistrationRecord
+ {
+ IsValid = true,
+ IsRegistered = true
+ });
}
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index b26732441..ff102b0f7 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -51,10 +51,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>());
}
+ // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+ private const char StreamIdDelimeter = '_';
+ private const string StreamIdDelimeterString = "_";
+
private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(ILiveTvItem item, CancellationToken cancellationToken)
{
IEnumerable<MediaSourceInfo> sources;
+ var forceRequireOpening = false;
+
try
{
if (item is ILiveTvRecording)
@@ -74,6 +80,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
sources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false)
.ToList();
+
+ forceRequireOpening = true;
}
var list = sources.ToList();
@@ -82,14 +90,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv
foreach (var source in list)
{
source.Type = MediaSourceType.Default;
- source.RequiresOpening = true;
source.BufferMs = source.BufferMs ?? 1500;
- var openKeys = new List<string>();
- openKeys.Add(item.GetType().Name);
- openKeys.Add(item.Id.ToString("N"));
- openKeys.Add(source.Id ?? string.Empty);
- source.OpenToken = string.Join("|", openKeys.ToArray());
+ if (source.RequiresOpening || forceRequireOpening)
+ {
+ source.RequiresOpening = true;
+ }
+
+ if (source.RequiresOpening)
+ {
+ var openKeys = new List<string>();
+ openKeys.Add(item.GetType().Name);
+ openKeys.Add(item.Id.ToString("N"));
+ openKeys.Add(source.Id ?? string.Empty);
+ source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray());
+ }
// Dummy this up so that direct play checks can still run
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
@@ -108,7 +123,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
MediaSourceInfo stream;
const bool isAudio = false;
- var keys = openToken.Split(new[] { '|' }, 3);
+ var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
diff --git a/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
index de2351434..d8d91c2f9 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
@@ -1,8 +1,10 @@
-using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.ScheduledTasks;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Tasks;
+using MediaBrowser.Model.LiveTv;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.LiveTv
@@ -10,10 +12,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask, IHasKey
{
private readonly ILiveTvManager _liveTvManager;
+ private readonly IConfigurationManager _config;
- public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager)
+ public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
{
_liveTvManager = liveTvManager;
+ _config = config;
}
public string Name
@@ -42,17 +46,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
return new ITaskTrigger[]
{
- new StartupTrigger(),
-
- new SystemEventTrigger{ SystemEvent = SystemEvent.WakeFromSleep},
-
- new IntervalTrigger{ Interval = TimeSpan.FromHours(4)}
+ new IntervalTrigger{ Interval = TimeSpan.FromHours(12)}
};
}
+ private LiveTvOptions GetConfiguration()
+ {
+ return _config.GetConfiguration<LiveTvOptions>("livetv");
+ }
+
public bool IsHidden
{
- get { return _liveTvManager.Services.Count == 0; }
+ get { return _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Count(i => i.IsEnabled) == 0; }
}
public bool IsEnabled
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
new file mode 100644
index 000000000..909e2bba5
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -0,0 +1,218 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
+{
+ public abstract class BaseTunerHost
+ {
+ protected readonly IConfigurationManager Config;
+ protected readonly ILogger Logger;
+
+ private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
+ new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
+
+ public BaseTunerHost(IConfigurationManager config, ILogger logger)
+ {
+ Config = config;
+ Logger = logger;
+ }
+
+ protected abstract Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+ public abstract string Type { get; }
+
+ public async Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
+ {
+ ChannelCache cache = null;
+ var key = tuner.Id;
+
+ if (enableCache && !string.IsNullOrWhiteSpace(key) && _channelCache.TryGetValue(key, out cache))
+ {
+ if ((DateTime.UtcNow - cache.Date) < TimeSpan.FromMinutes(60))
+ {
+ return cache.Channels.ToList();
+ }
+ }
+
+ var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
+ var list = result.ToList();
+
+ if (!string.IsNullOrWhiteSpace(key))
+ {
+ cache = cache ?? new ChannelCache();
+ cache.Date = DateTime.UtcNow;
+ cache.Channels = list;
+ _channelCache.AddOrUpdate(key, cache, (k, v) => cache);
+ }
+
+ return list;
+ }
+
+ private List<TunerHostInfo> GetTunerHosts()
+ {
+ return GetConfiguration().TunerHosts
+ .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+ }
+
+ public async Task<IEnumerable<ChannelInfo>> GetChannels(CancellationToken cancellationToken)
+ {
+ var list = new List<ChannelInfo>();
+
+ var hosts = GetTunerHosts();
+
+ foreach (var host in hosts)
+ {
+ try
+ {
+ var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
+ var newChannels = channels.Where(i => !list.Any(l => string.Equals(i.Id, l.Id, StringComparison.OrdinalIgnoreCase))).ToList();
+
+ list.AddRange(newChannels);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error getting channel list", ex);
+ }
+ }
+
+ return list;
+ }
+
+ protected abstract Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
+
+ public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
+ {
+ if (IsValidChannelId(channelId))
+ {
+ var hosts = GetTunerHosts();
+
+ var hostsWithChannel = new List<TunerHostInfo>();
+
+ foreach (var host in hosts)
+ {
+ var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
+
+ if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
+ {
+ hostsWithChannel.Add(host);
+ }
+ }
+
+ foreach (var host in hostsWithChannel)
+ {
+ // Check to make sure the tuner is available
+ // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error
+ if (hostsWithChannel.Count > 1 && !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
+ {
+ Logger.Error("Tuner is not currently available");
+ continue;
+ }
+
+ var mediaSources = await GetChannelStreamMediaSources(host, channelId, cancellationToken).ConfigureAwait(false);
+
+ // Prefix the id with the host Id so that we can easily find it
+ foreach (var mediaSource in mediaSources)
+ {
+ mediaSource.Id = host.Id + mediaSource.Id;
+ }
+
+ return mediaSources;
+ }
+ }
+
+ return new List<MediaSourceInfo>();
+ }
+
+ protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
+
+ public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
+ {
+ if (IsValidChannelId(channelId))
+ {
+ var hosts = GetTunerHosts();
+
+ var hostsWithChannel = new List<TunerHostInfo>();
+
+ foreach (var host in hosts)
+ {
+ if (string.IsNullOrWhiteSpace(streamId))
+ {
+ var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
+
+ if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
+ {
+ hostsWithChannel.Add(host);
+ }
+ }
+ else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase))
+ {
+ hostsWithChannel = new List<TunerHostInfo> { host };
+ streamId = streamId.Substring(host.Id.Length);
+ break;
+ }
+ }
+
+ foreach (var host in hostsWithChannel)
+ {
+ // Check to make sure the tuner is available
+ // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error
+ // If a streamId is specified then availibility has already been checked in GetChannelStreamMediaSources
+ if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1)
+ {
+ if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
+ {
+ Logger.Error("Tuner is not currently available");
+ continue;
+ }
+ }
+
+ var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
+
+ if (stream != null)
+ {
+ return stream;
+ }
+ }
+ }
+
+ throw new LiveTvConflictException();
+ }
+
+ protected async Task<bool> IsAvailable(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
+ {
+ try
+ {
+ return await IsAvailableInternal(tuner, channelId, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error checking tuner availability", ex);
+ return false;
+ }
+ }
+
+ protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
+
+ protected abstract bool IsValidChannelId(string channelId);
+
+ protected LiveTvOptions GetConfiguration()
+ {
+ return Config.GetConfiguration<LiveTvOptions>("livetv");
+ }
+
+ private class ChannelCache
+ {
+ public DateTime Date;
+ public List<ChannelInfo> Channels;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
new file mode 100644
index 000000000..c74220137
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
@@ -0,0 +1,122 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class HdHomerunDiscovery : IServerEntryPoint
+ {
+ private readonly IDeviceDiscovery _deviceDiscovery;
+ private readonly IServerConfigurationManager _config;
+ private readonly ILogger _logger;
+ private readonly ILiveTvManager _liveTvManager;
+ private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
+
+ public HdHomerunDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager)
+ {
+ _deviceDiscovery = deviceDiscovery;
+ _config = config;
+ _logger = logger;
+ _liveTvManager = liveTvManager;
+ }
+
+ public void Run()
+ {
+ _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
+ }
+
+ void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
+ {
+ string server = null;
+ if (e.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ string location;
+ if (e.Headers.TryGetValue("Location", out location))
+ {
+ //_logger.Debug("HdHomerun found at {0}", location);
+
+ // Just get the beginning of the url
+ Uri uri;
+ if (Uri.TryCreate(location, UriKind.Absolute, out uri))
+ {
+ var apiUrl = location.Replace(uri.LocalPath, String.Empty, StringComparison.OrdinalIgnoreCase)
+ .TrimEnd('/');
+
+ //_logger.Debug("HdHomerun api url: {0}", apiUrl);
+ AddDevice(apiUrl);
+ }
+ }
+ }
+ }
+
+ private async void AddDevice(string url)
+ {
+ await _semaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ var options = GetConfiguration();
+
+ if (options.TunerHosts.Any(i =>
+ string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) &&
+ UriEquals(i.Url, url)))
+ {
+ return;
+ }
+
+ // Strip off the port
+ url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/');
+
+ await _liveTvManager.SaveTunerHost(new TunerHostInfo
+ {
+ Type = HdHomerunHost.DeviceType,
+ Url = url
+
+ }).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error saving device", ex);
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ private bool UriEquals(string savedUri, string location)
+ {
+ return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase);
+ }
+
+ private string NormalizeUrl(string url)
+ {
+ if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ url = "http://" + url;
+ }
+
+ url = url.TrimEnd('/');
+
+ // Strip off the port
+ return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped);
+ }
+
+ private LiveTvOptions GetConfiguration()
+ {
+ return _config.GetConfiguration<LiveTvOptions>("livetv");
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
new file mode 100644
index 000000000..bccb0db0a
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -0,0 +1,406 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class HdHomerunHost : BaseTunerHost, ITunerHost
+ {
+ private readonly IHttpClient _httpClient;
+ private readonly IJsonSerializer _jsonSerializer;
+
+ public HdHomerunHost(IConfigurationManager config, ILogger logger, IHttpClient httpClient, IJsonSerializer jsonSerializer)
+ : base(config, logger)
+ {
+ _httpClient = httpClient;
+ _jsonSerializer = jsonSerializer;
+ }
+
+ public string Name
+ {
+ get { return "HD Homerun"; }
+ }
+
+ public override string Type
+ {
+ get { return DeviceType; }
+ }
+
+ public static string DeviceType
+ {
+ get { return "hdhomerun"; }
+ }
+
+ private const string ChannelIdPrefix = "hdhr_";
+
+ protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
+ {
+ var options = new HttpRequestOptions
+ {
+ Url = string.Format("{0}/lineup.json", GetApiUrl(info, false)),
+ CancellationToken = cancellationToken
+ };
+ using (var stream = await _httpClient.Get(options))
+ {
+ var root = _jsonSerializer.DeserializeFromStream<List<Channels>>(stream);
+
+ if (root != null)
+ {
+ var result = root.Select(i => new ChannelInfo
+ {
+ Name = i.GuideName,
+ Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
+ Id = ChannelIdPrefix + i.GuideNumber.ToString(CultureInfo.InvariantCulture),
+ IsFavorite = i.Favorite
+
+ });
+
+ if (info.ImportFavoritesOnly)
+ {
+ result = result.Where(i => (i.IsFavorite ?? true)).ToList();
+ }
+
+ return result;
+ }
+ return new List<ChannelInfo>();
+ }
+ }
+
+ private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken)
+ {
+ string model = null;
+
+ using (var stream = await _httpClient.Get(new HttpRequestOptions()
+ {
+ Url = string.Format("{0}/", GetApiUrl(info, false)),
+ CancellationToken = cancellationToken,
+ CacheLength = TimeSpan.FromDays(1),
+ CacheMode = CacheMode.Unconditional
+ }))
+ {
+ using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
+ {
+ while (!sr.EndOfStream)
+ {
+ string line = StripXML(sr.ReadLine());
+ if (line.StartsWith("Model:")) { model = line.Replace("Model: ", ""); }
+ //if (line.StartsWith("Device ID:")) { deviceID = line.Replace("Device ID: ", ""); }
+ //if (line.StartsWith("Firmware:")) { firmware = line.Replace("Firmware: ", ""); }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
+ {
+ var model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
+
+ using (var stream = await _httpClient.Get(new HttpRequestOptions()
+ {
+ Url = string.Format("{0}/tuners.html", GetApiUrl(info, false)),
+ CancellationToken = cancellationToken
+ }))
+ {
+ var tuners = new List<LiveTvTunerInfo>();
+ using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
+ {
+ while (!sr.EndOfStream)
+ {
+ string line = StripXML(sr.ReadLine());
+ if (line.Contains("Channel"))
+ {
+ LiveTvTunerStatus status;
+ var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = line.Substring(0, index - 1);
+ var currentChannel = line.Substring(index + 7);
+ if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; }
+ tuners.Add(new LiveTvTunerInfo
+ {
+ Name = name,
+ SourceType = string.IsNullOrWhiteSpace(model) ? Name : model,
+ ProgramName = currentChannel,
+ Status = status
+ });
+ }
+ }
+ }
+ return tuners;
+ }
+ }
+
+ public async Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
+ {
+ var list = new List<LiveTvTunerInfo>();
+
+ foreach (var host in GetConfiguration().TunerHosts
+ .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
+ {
+ try
+ {
+ list.AddRange(await GetTunerInfos(host, cancellationToken).ConfigureAwait(false));
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error getting tuner info", ex);
+ }
+ }
+
+ return list;
+ }
+
+ private string GetApiUrl(TunerHostInfo info, bool isPlayback)
+ {
+ var url = info.Url;
+
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ throw new ArgumentException("Invalid tuner info");
+ }
+
+ if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ url = "http://" + url;
+ }
+
+ var uri = new Uri(url);
+
+ if (isPlayback)
+ {
+ var builder = new UriBuilder(uri);
+ builder.Port = 5004;
+ uri = builder.Uri;
+ }
+
+ return uri.AbsoluteUri.TrimEnd('/');
+ }
+
+ private static string StripXML(string source)
+ {
+ char[] buffer = new char[source.Length];
+ int bufferIndex = 0;
+ bool inside = false;
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ char let = source[i];
+ if (let == '<')
+ {
+ inside = true;
+ continue;
+ }
+ if (let == '>')
+ {
+ inside = false;
+ continue;
+ }
+ if (!inside)
+ {
+ buffer[bufferIndex] = let;
+ bufferIndex++;
+ }
+ }
+ return new string(buffer, 0, bufferIndex);
+ }
+
+ private class Channels
+ {
+ public string GuideNumber { get; set; }
+ public string GuideName { get; set; }
+ public string URL { get; set; }
+ public bool Favorite { get; set; }
+ public bool DRM { get; set; }
+ }
+
+ private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, string profile)
+ {
+ int? width = null;
+ int? height = null;
+ bool isInterlaced = true;
+ var videoCodec = "mpeg2video";
+ int? videoBitrate = null;
+
+ if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase))
+ {
+ width = 1280;
+ height = 720;
+ isInterlaced = false;
+ videoCodec = "h264";
+ videoBitrate = 2000000;
+ }
+ else if (string.Equals(profile, "heavy", StringComparison.OrdinalIgnoreCase))
+ {
+ width = 1920;
+ height = 1080;
+ isInterlaced = false;
+ videoCodec = "h264";
+ videoBitrate = 8000000;
+ }
+ else if (string.Equals(profile, "internet720", StringComparison.OrdinalIgnoreCase))
+ {
+ width = 1280;
+ height = 720;
+ isInterlaced = false;
+ videoCodec = "h264";
+ videoBitrate = 5000000;
+ }
+ else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase))
+ {
+ width = 1280;
+ height = 720;
+ isInterlaced = false;
+ videoCodec = "h264";
+ videoBitrate = 2500000;
+ }
+ else if (string.Equals(profile, "internet480", StringComparison.OrdinalIgnoreCase))
+ {
+ width = 848;
+ height = 480;
+ isInterlaced = false;
+ videoCodec = "h264";
+ videoBitrate = 2000000;
+ }
+ else if (string.Equals(profile, "internet360", StringComparison.OrdinalIgnoreCase))
+ {
+ width = 640;
+ height = 360;
+ isInterlaced = false;
+ videoCodec = "h264";
+ videoBitrate = 1500000;
+ }
+ else if (string.Equals(profile, "internet240", StringComparison.OrdinalIgnoreCase))
+ {
+ width = 432;
+ height = 240;
+ isInterlaced = false;
+ videoCodec = "h264";
+ videoBitrate = 1000000;
+ }
+
+ var url = GetApiUrl(info, true) + "/auto/v" + channelId;
+
+ if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
+ {
+ url += "?transcode=" + profile;
+ }
+
+ var mediaSource = new MediaSourceInfo
+ {
+ Path = url,
+ Protocol = MediaProtocol.Http,
+ MediaStreams = new List<MediaStream>
+ {
+ new MediaStream
+ {
+ Type = MediaStreamType.Video,
+ // Set the index to -1 because we don't know the exact index of the video stream within the container
+ Index = -1,
+ IsInterlaced = isInterlaced,
+ Codec = videoCodec,
+ Width = width,
+ Height = height,
+ BitRate = videoBitrate
+
+ },
+ 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 = 128000
+ }
+ },
+ RequiresOpening = false,
+ RequiresClosing = false,
+ BufferMs = 1000,
+ Container = "ts",
+ Id = profile
+ };
+
+ return mediaSource;
+ }
+
+ protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
+ {
+ var list = new List<MediaSourceInfo>();
+
+ if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return list;
+ }
+ channelId = channelId.Substring(ChannelIdPrefix.Length);
+
+ list.Add(GetMediaSource(info, channelId, "native"));
+
+ try
+ {
+ string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
+ model = model ?? string.Empty;
+
+ if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ list.Insert(0, GetMediaSource(info, channelId, "heavy"));
+
+ list.Add(GetMediaSource(info, channelId, "internet480"));
+ list.Add(GetMediaSource(info, channelId, "internet360"));
+ list.Add(GetMediaSource(info, channelId, "internet240"));
+ list.Add(GetMediaSource(info, channelId, "mobile"));
+ }
+ }
+ catch (Exception ex)
+ {
+
+ }
+
+ return list;
+ }
+
+ protected override bool IsValidChannelId(string channelId)
+ {
+ return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
+ }
+
+ protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
+ {
+ Logger.Debug("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty);
+
+ if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ channelId = channelId.Substring(ChannelIdPrefix.Length);
+
+ return GetMediaSource(info, channelId, streamId);
+ }
+
+ public async Task Validate(TunerHostInfo info)
+ {
+ if (info.IsEnabled)
+ {
+ await GetChannels(info, false, CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+
+ protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
+ {
+ var info = await GetTunerInfos(tuner, cancellationToken).ConfigureAwait(false);
+
+ return info.Any(i => i.Status == LiveTvTunerStatus.Available || string.Equals(i.ChannelId, channelId, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
new file mode 100644
index 000000000..3783e4b08
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -0,0 +1,199 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
+{
+ public class M3UTunerHost : BaseTunerHost, ITunerHost
+ {
+ public M3UTunerHost(IConfigurationManager config, ILogger logger)
+ : base(config, logger)
+ {
+ }
+
+ public override string Type
+ {
+ get { return "m3u"; }
+ }
+
+ public string Name
+ {
+ get { return "M3U Tuner"; }
+ }
+
+ private const string ChannelIdPrefix = "m3u_";
+
+ protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
+ {
+ var url = info.Url;
+ var urlHash = url.GetMD5().ToString("N");
+
+ string line;
+ // Read the file and display it line by line.
+ var file = new StreamReader(url);
+ var channels = new List<M3UChannel>();
+
+ string channnelName = null;
+ string channelNumber = null;
+
+ while ((line = file.ReadLine()) != null)
+ {
+ line = line.Trim();
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = line.Split(new[] { ':' }, 2).Last().Split(new[] { ',' }, 2);
+ channelNumber = parts[0];
+ channnelName = parts[1];
+ }
+ else if (!string.IsNullOrWhiteSpace(channelNumber))
+ {
+ channels.Add(new M3UChannel
+ {
+ Name = channnelName,
+ Number = channelNumber,
+ Id = ChannelIdPrefix + urlHash + channelNumber,
+ Path = line
+ });
+
+ channelNumber = null;
+ channnelName = null;
+ }
+ }
+ file.Close();
+ return channels;
+ }
+
+ public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
+ {
+ var list = GetConfiguration().TunerHosts
+ .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
+ .Select(i => new LiveTvTunerInfo()
+ {
+ Name = Name,
+ SourceType = Type,
+ Status = LiveTvTunerStatus.Available,
+ Id = i.Url.GetMD5().ToString("N"),
+ Url = i.Url
+ })
+ .ToList();
+
+ return Task.FromResult(list);
+ }
+
+ protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
+ {
+ var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false);
+
+ return sources.First();
+ }
+
+ class M3UChannel : ChannelInfo
+ {
+ public string Path { get; set; }
+
+ public M3UChannel()
+ {
+ }
+ }
+
+ public async Task Validate(TunerHostInfo info)
+ {
+ if (!File.Exists(info.Url))
+ {
+ throw new FileNotFoundException();
+ }
+ }
+
+ protected override bool IsValidChannelId(string channelId)
+ {
+ return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
+ }
+
+ protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
+ {
+ var urlHash = info.Url.GetMD5().ToString("N");
+ var prefix = ChannelIdPrefix + urlHash;
+ if (!channelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ //channelId = channelId.Substring(prefix.Length);
+
+ var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false);
+ var m3uchannels = channels.Cast<M3UChannel>();
+ var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase));
+ if (channel != null)
+ {
+ var path = channel.Path;
+ MediaProtocol protocol = MediaProtocol.File;
+ if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ protocol = MediaProtocol.Http;
+ }
+ else if (path.StartsWith("rtmp", StringComparison.OrdinalIgnoreCase))
+ {
+ protocol = MediaProtocol.Rtmp;
+ }
+ else if (path.StartsWith("rtsp", StringComparison.OrdinalIgnoreCase))
+ {
+ protocol = MediaProtocol.Rtsp;
+ }
+
+ var mediaSource = new MediaSourceInfo
+ {
+ Path = channel.Path,
+ Protocol = protocol,
+ MediaStreams = new List<MediaStream>
+ {
+ new MediaStream
+ {
+ Type = MediaStreamType.Video,
+ // Set the index to -1 because we don't know the exact index of the video stream within the container
+ Index = -1,
+ IsInterlaced = true
+ },
+ new MediaStream
+ {
+ Type = MediaStreamType.Audio,
+ // Set the index to -1 because we don't know the exact index of the audio stream within the container
+ Index = -1
+
+ }
+ },
+ RequiresOpening = false,
+ RequiresClosing = false
+ };
+
+ return new List<MediaSourceInfo> { mediaSource };
+ }
+ return new List<MediaSourceInfo> { };
+ }
+
+ protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(true);
+ }
+ }
+}