瀏覽代碼

start pulling in EmbyTV

Luke Pulverenti 10 年之前
父節點
當前提交
20b990dc9a

+ 12 - 4
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -432,7 +432,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
                     var httpResponse = (HttpWebResponse)response;
 
-                    EnsureSuccessStatusCode(httpResponse, options);
+                    EnsureSuccessStatusCode(client, httpResponse, options);
 
                     options.CancellationToken.ThrowIfCancellationRequested();
 
@@ -443,7 +443,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 {
                     var httpResponse = (HttpWebResponse)response;
 
-                    EnsureSuccessStatusCode(httpResponse, options);
+                    EnsureSuccessStatusCode(client, httpResponse, options);
 
                     options.CancellationToken.ThrowIfCancellationRequested();
 
@@ -629,7 +629,8 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 {
                     var httpResponse = (HttpWebResponse)response;
 
-                    EnsureSuccessStatusCode(httpResponse, options);
+                    var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
+                    EnsureSuccessStatusCode(client, httpResponse, options);
 
                     options.CancellationToken.ThrowIfCancellationRequested();
 
@@ -803,13 +804,20 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             return exception;
         }
 
-        private void EnsureSuccessStatusCode(HttpWebResponse response, HttpRequestOptions options)
+        private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options)
         {
             var statusCode = response.StatusCode;
+
             var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299;
 
             if (!isSuccessful)
             {
+                if ((int) statusCode == 429)
+                {
+                    client.LastTimeout = DateTime.UtcNow;
+                }
+
+                if (statusCode == HttpStatusCode.RequestEntityTooLarge)
                 if (options.LogErrorResponseBody)
                 {
                     try

+ 5 - 1
MediaBrowser.Controller/LiveTv/ChannelInfo.cs

@@ -48,6 +48,10 @@ namespace MediaBrowser.Controller.LiveTv
         /// </summary>
         /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
         public bool? HasImage { get; set; }
-
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is favorite.
+        /// </summary>
+        /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
+        public bool? IsFavorite { get; set; }
     }
 }

+ 7 - 0
MediaBrowser.Controller/LiveTv/IListingsProvider.cs

@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public interface IListingsProvider
+    {
+    }
+}

+ 50 - 0
MediaBrowser.Controller/LiveTv/ITunerHost.cs

@@ -0,0 +1,50 @@
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public interface ITunerHost
+    {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+        /// <summary>
+        /// Gets the type.
+        /// </summary>
+        /// <value>The type.</value>
+        string Type { get; }
+        /// <summary>
+        /// Gets the tuner hosts.
+        /// </summary>
+        /// <returns>List&lt;TunerHostInfo&gt;.</returns>
+        List<TunerHostInfo> GetTunerHosts();
+        /// <summary>
+        /// Gets the channels.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;IEnumerable&lt;ChannelInfo&gt;&gt;.</returns>
+        Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo info, CancellationToken cancellationToken);
+        /// <summary>
+        /// Gets the tuner infos.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;List&lt;LiveTvTunerInfo&gt;&gt;.</returns>
+        Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken);
+        /// <summary>
+        /// Gets the channel stream.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="channelId">The channel identifier.</param>
+        /// <param name="streamId">The stream identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
+        Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken);
+    }
+}

+ 2 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -198,7 +198,9 @@
     <Compile Include="Library\NameExtensions.cs" />
     <Compile Include="Library\PlaybackStopEventArgs.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
+    <Compile Include="LiveTv\IListingsProvider.cs" />
     <Compile Include="LiveTv\ILiveTvItem.cs" />
+    <Compile Include="LiveTv\ITunerHost.cs" />
     <Compile Include="LiveTv\RecordingGroup.cs" />
     <Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" />
     <Compile Include="LiveTv\ILiveTvRecording.cs" />

+ 12 - 1
MediaBrowser.Model/LiveTv/LiveTvOptions.cs

@@ -1,13 +1,24 @@
-namespace MediaBrowser.Model.LiveTv
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.LiveTv
 {
     public class LiveTvOptions
     {
         public int? GuideDays { get; set; }
         public bool EnableMovieProviders { get; set; }
+        public List<TunerHostInfo> TunerHosts { get; set; }
+        public string RecordingPath { get; set; }
 
         public LiveTvOptions()
         {
             EnableMovieProviders = true;
+            TunerHosts = new List<TunerHostInfo>();
         }
     }
+
+    public class TunerHostInfo
+    {
+        public string Url { get; set; }
+        public string Type { get; set; }
+    }
 }

+ 9 - 3
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);

+ 507 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -0,0 +1,507 @@
+using MediaBrowser.Common;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+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, IDisposable
+    {
+        private readonly ILogger _logger;
+        private readonly IHttpClient _httpClient;
+        private readonly IConfigurationManager _config;
+        private readonly IJsonSerializer _jsonSerializer;
+
+        private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>();
+        private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
+        private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
+        private readonly TimerManager _timerProvider;
+
+        public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IConfigurationManager config)
+        {
+            _logger = logger;
+            _httpClient = httpClient;
+            _config = config;
+            _jsonSerializer = jsonSerializer;
+            _tunerHosts.AddRange(appHost.GetExports<ITunerHost>());
+
+            _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 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 host in _tunerHosts)
+            {
+                foreach (var hostInstance in host.GetTunerHosts())
+                {
+                    try
+                    {
+                        var tuners = await host.GetTunerInfos(hostInstance, cancellationToken).ConfigureAwait(false);
+
+                        list.AddRange(tuners);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error getting tuners", ex);
+                    }
+                }
+            }
+
+            status.Tuners = list;
+            status.Status = LiveTvServiceStatus.Ok;
+            return status;
+        }
+
+        public async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
+        {
+            var list = new List<ChannelInfo>();
+
+            foreach (var host in _tunerHosts)
+            {
+                foreach (var hostInstance in host.GetTunerHosts())
+                {
+                    try
+                    {
+                        var channels = await host.GetChannels(hostInstance, cancellationToken).ConfigureAwait(false);
+
+                        list.AddRange(channels);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error getting channels", ex);
+                    }
+                }
+            }
+
+            return list;
+        }
+
+        public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
+        {
+            var remove = _seriesTimerProvider.GetAll().SingleOrDefault(r => r.Id == timerId);
+            if (remove != null)
+            {
+                _seriesTimerProvider.Delete(remove);
+            }
+            return Task.FromResult(true);
+        }
+
+        private void CancelTimerInternal(string timerId)
+        {
+            var remove = _timerProvider.GetAll().SingleOrDefault(r => r.Id == timerId);
+            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 Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
+        {
+            var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase));
+            if (remove != null)
+            {
+                try
+                {
+                    File.Delete(remove.Path);
+                }
+                catch (DirectoryNotFoundException)
+                {
+
+                }
+                catch (FileNotFoundException)
+                {
+
+                }
+                _recordingProvider.Delete(remove);
+            }
+            return Task.FromResult(true);
+        }
+
+        public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+        {
+            info.Id = Guid.NewGuid().ToString("N");
+            _timerProvider.Add(info);
+            return Task.FromResult(0);
+        }
+
+        public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+        {
+            info.Id = info.ProgramId.Substring(0, 10);
+
+            UpdateTimersForSeriesTimer(info);
+            _seriesTimerProvider.Add(info);
+            return Task.FromResult(true);
+        }
+
+        public Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+        {
+            _seriesTimerProvider.Update(info);
+            UpdateTimersForSeriesTimer(info);
+            return Task.FromResult(true);
+        }
+
+        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 defaults = new SeriesTimerInfo()
+            {
+                PostPaddingSeconds = 60,
+                PrePaddingSeconds = 60,
+                RecordAnyChannel = false,
+                RecordAnyTime = false,
+                RecordNewOnly = false
+            };
+            return Task.FromResult(defaults);
+        }
+
+        public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
+        {
+            return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
+        }
+
+        public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+        {
+            throw new NotImplementedException();
+        }
+
+        public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
+        {
+            throw new NotImplementedException();
+        }
+
+        public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
+        {
+            throw new NotImplementedException();
+        }
+
+        public Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
+        {
+            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)
+        {
+            try
+            {
+                var cancellationTokenSource = new CancellationTokenSource();
+
+                if (_activeRecordings.TryAdd(e.Argument.Id, cancellationTokenSource))
+                {
+                    await RecordStream(e.Argument, cancellationTokenSource.Token).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error recording stream", ex);
+            }
+        }
+
+        private async Task RecordStream(TimerInfo timer, CancellationToken cancellationToken)
+        {
+            var mediaStreamInfo = await GetChannelStream(timer.ChannelId, "none", CancellationToken.None);
+            var duration = (timer.EndDate - RecordingHelper.GetStartTime(timer)).TotalSeconds + timer.PrePaddingSeconds;
+
+            HttpRequestOptions httpRequestOptions = new HttpRequestOptions()
+            {
+                Url = mediaStreamInfo.Path + "?duration=" + duration
+            };
+
+            var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
+            var recordPath = RecordingPath;
+            if (info.IsMovie)
+            {
+                recordPath = Path.Combine(recordPath, "Movies", RecordingHelper.RemoveSpecialCharacters(info.Name));
+            }
+            else
+            {
+                recordPath = Path.Combine(recordPath, "TV", RecordingHelper.RemoveSpecialCharacters(info.Name));
+            }
+
+            recordPath = Path.Combine(recordPath, RecordingHelper.GetRecordingName(timer, info));
+            Directory.CreateDirectory(Path.GetDirectoryName(recordPath));
+
+            var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, info.Id, StringComparison.OrdinalIgnoreCase));
+
+            if (recording == null)
+            {
+                recording = new RecordingInfo()
+                {
+                    ChannelId = info.ChannelId,
+                    Id = info.Id,
+                    StartDate = info.StartDate,
+                    EndDate = info.EndDate,
+                    Genres = info.Genres ?? null,
+                    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 ?? false,
+                    ImagePath = info.ImagePath ?? null,
+                    ImageUrl = info.ImageUrl,
+                    OriginalAirDate = info.OriginalAirDate,
+                    Status = RecordingStatus.Scheduled,
+                    Overview = info.Overview,
+                    SeriesTimerId = info.Id.Substring(0, 10)
+                };
+                _recordingProvider.Add(recording);
+            }
+
+            recording.Path = recordPath;
+            recording.Status = RecordingStatus.InProgress;
+            _recordingProvider.Update(recording);
+
+            try
+            {
+                httpRequestOptions.BufferContent = false;
+                httpRequestOptions.CancellationToken = cancellationToken;
+                _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, 4096, cancellationToken);
+                    }
+                }
+
+                recording.Status = RecordingStatus.Completed;
+            }
+            catch (OperationCanceledException)
+            {
+                recording.Status = RecordingStatus.Cancelled;
+            }
+            catch
+            {
+                recording.Status = RecordingStatus.Error;
+            }
+
+            _recordingProvider.Update(recording);
+            _timerProvider.Delete(timer);
+            _logger.Info("Recording was a success");
+        }
+
+        private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
+        {
+            var epgData = GetEpgDataForChannel(channelId);
+            if (epgData.Any())
+            {
+                return epgData.FirstOrDefault(p => p.Id == programId);
+            }
+            return null;
+        }
+
+        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 void UpdateTimersForSeriesTimer(SeriesTimerInfo seriesTimer)
+        {
+            List<ProgramInfo> epgData;
+            if (seriesTimer.RecordAnyChannel)
+            {
+                epgData = GetEpgDataForAllChannels();
+            }
+            else
+            {
+                epgData = GetEpgDataForChannel(seriesTimer.ChannelId);
+            }
+
+            var newTimers = RecordingHelper.GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll(), _logger);
+
+            var existingTimers = _timerProvider.GetAll()
+                .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
+                .ToList();
+
+            foreach (var timer in newTimers)
+            {
+                _timerProvider.AddOrUpdate(timer);
+            }
+
+            var newTimerIds = newTimers.Select(i => i.Id).ToList();
+
+            foreach (var timer in existingTimers)
+            {
+                if (!newTimerIds.Contains(timer.Id, StringComparer.OrdinalIgnoreCase))
+                {
+                    CancelTimerInternal(timer.Id);
+                }
+            }
+        }
+
+        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> GetEpgDataForAllChannels()
+        {
+            List<ProgramInfo> channelEpg = new List<ProgramInfo>();
+            DirectoryInfo dir = new DirectoryInfo(Path.Combine(DataPath, "epg"));
+            List<string> channels = dir.GetFiles("*").Where(i => string.Equals(i.Extension, ".json", StringComparison.OrdinalIgnoreCase)).Select(f => f.Name).ToList();
+            foreach (var channel in channels)
+            {
+                channelEpg.AddRange(GetEpgDataForChannel(channel));
+            }
+            return channelEpg;
+        }
+
+        public void Dispose()
+        {
+            foreach (var pair in _activeRecordings.ToList())
+            {
+                pair.Value.Cancel();
+            }
+        }
+    }
+}

+ 115 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

@@ -0,0 +1,115 @@
+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)
+        {
+            lock (_fileDataLock)
+            {
+                _jsonSerializer.SerializeToFile(newList, _dataPath + ".json");
+                _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);
+        }
+    }
+}

+ 119 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs

@@ -0,0 +1,119 @@
+using System.Text;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+    internal class RecordingHelper
+    {
+        public static List<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> epgData, IReadOnlyList<RecordingInfo> currentRecordings, ILogger logger)
+        {
+            List<TimerInfo> timers = new List<TimerInfo>();
+
+            // Filtered Per Show
+            var filteredEpg = epgData.Where(epg => epg.Id.Substring(0, 10) == seriesTimer.Id);
+
+            if (!seriesTimer.RecordAnyTime)
+            {
+                filteredEpg = filteredEpg.Where(epg => (seriesTimer.StartDate.TimeOfDay == epg.StartDate.TimeOfDay));
+            }
+
+            if (seriesTimer.RecordNewOnly)
+            {
+                filteredEpg = filteredEpg.Where(epg => !epg.IsRepeat); //Filtered by New only
+            }
+
+            if (!seriesTimer.RecordAnyChannel)
+            {
+                filteredEpg = filteredEpg.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
+            }
+
+            filteredEpg = filteredEpg.Where(epg => seriesTimer.Days.Contains(epg.StartDate.DayOfWeek));
+
+            filteredEpg = filteredEpg.Where(epg => currentRecordings.All(r => r.Id.Substring(0, 14) != epg.Id.Substring(0, 14))); //filtered recordings already running
+
+            filteredEpg = filteredEpg.GroupBy(epg => epg.Id.Substring(0, 14)).Select(g => g.First()).ToList();
+
+            foreach (var epg in filteredEpg)
+            {
+                timers.Add(CreateTimer(epg, seriesTimer));
+            }
+
+            return timers;
+        }
+
+        public static DateTime GetStartTime(TimerInfo timer)
+        {
+            if (timer.StartDate.AddSeconds(-timer.PrePaddingSeconds + 1) < DateTime.UtcNow)
+            {
+                return DateTime.UtcNow.AddSeconds(1);
+            }
+            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 + ".ts");
+            }
+            var fancyName = info.Name;
+            if (info.ProductionYear != null)
+            {
+                fancyName += "_(" + info.ProductionYear + ")";
+            }
+            if (info.IsSeries)
+            {
+                fancyName += "_" + info.EpisodeTitle.Replace("Season: ", "S").Replace(" Episode: ", "E");
+            }
+            if (info.IsHD ?? false)
+            {
+                fancyName += "_HD";
+            }
+            if (info.OriginalAirDate != null)
+            {
+                fancyName += "_" + info.OriginalAirDate.Value.ToString("yyyy-MM-dd");
+            }
+            return RemoveSpecialCharacters(fancyName) + ".ts";
+        }
+
+        public static string RemoveSpecialCharacters(string str)
+        {
+            StringBuilder sb = new StringBuilder();
+            foreach (char c in str)
+            {
+                if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '.' || c == '_' || c == '-' || c == ' ')
+                {
+                    sb.Append(c);
+                }
+            }
+            return sb.ToString();
+        }
+    }
+}

+ 25 - 0
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);
+        }
+    }
+}

+ 114 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -0,0 +1,114 @@
+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();
+        }
+
+        public void StopTimers()
+        {
+            foreach (var pair in _timers.ToList())
+            {
+                pair.Value.Dispose();
+            }
+
+            _timers.Clear();
+        }
+
+        public override void Delete(TimerInfo item)
+        {
+            base.Delete(item);
+
+            Timer timer;
+            if (_timers.TryRemove(item.Id, out timer))
+            {
+                timer.Dispose();
+            }
+        }
+
+        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 timespan = RecordingHelper.GetStartTime(item) - DateTime.UtcNow;
+
+            var timer = new Timer(TimerCallback, item.Id, timespan, TimeSpan.Zero);
+
+            if (!_timers.TryAdd(item.Id, 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);
+            }
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -55,7 +55,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
         private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1);
 
-        private ConcurrentDictionary<Guid, Guid> _refreshedPrograms = new ConcurrentDictionary<Guid, Guid>();
+        private readonly ConcurrentDictionary<Guid, Guid> _refreshedPrograms = new ConcurrentDictionary<Guid, Guid>();
 
         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)
         {

+ 205 - 0
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun.cs

@@ -0,0 +1,205 @@
+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
+{
+    public class HdHomerun : ITunerHost
+    {
+        private readonly IHttpClient _httpClient;
+        private readonly ILogger _logger;
+        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IConfigurationManager _config;
+
+        public HdHomerun(IHttpClient httpClient, ILogger logger, IJsonSerializer jsonSerializer, IConfigurationManager config)
+        {
+            _httpClient = httpClient;
+            _logger = logger;
+            _jsonSerializer = jsonSerializer;
+            _config = config;
+        }
+
+        public string Name
+        {
+            get { return "HD Homerun"; }
+        }
+
+        public string Type
+        {
+            get { return "hdhomerun"; }
+        }
+
+        public async Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo info, CancellationToken cancellationToken)
+        {
+            var options = new HttpRequestOptions
+            {
+                Url = string.Format("{0}/lineup.json", GetApiUrl(info)),
+                CancellationToken = cancellationToken
+            };
+            using (var stream = await _httpClient.Get(options))
+            {
+                var root = _jsonSerializer.DeserializeFromStream<List<Channels>>(stream);
+
+                if (root != null)
+                {
+                    return root.Select(i => new ChannelInfo
+                    {
+                        Name = i.GuideName,
+                        Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
+                        Id = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
+                        IsFavorite = i.Favorite
+
+                    });
+                }
+                return new List<ChannelInfo>();
+            }
+        }
+
+        public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
+        {
+            var httpOptions = new HttpRequestOptions()
+            {
+                Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
+                CancellationToken = cancellationToken
+            };
+            using (var stream = await _httpClient.Get(httpOptions))
+            {
+                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 = Name,
+                                ProgramName = currentChannel,
+                                Status = status
+                            });
+                        }
+                    }
+                }
+                return tuners;
+            }
+        }
+
+        public string GetApiUrl(TunerHostInfo info)
+        {
+            var url = info.Url;
+
+            if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            {
+                url = "http://" + url;
+            }
+
+            return url.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 LiveTvOptions GetConfiguration()
+        {
+            return _config.GetConfiguration<LiveTvOptions>("livetv");
+        }
+
+        public List<TunerHostInfo> GetTunerHosts()
+        {
+            return GetConfiguration().TunerHosts.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)).ToList();
+        }
+
+        public async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
+        {
+            var channels = await GetChannels(info, cancellationToken).ConfigureAwait(false);
+            var tuners = await GetTunerInfos(info, cancellationToken).ConfigureAwait(false);
+
+            var channel = channels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase));
+            if (channel != null)
+            {
+                if (tuners.FindIndex(t => t.Status == LiveTvTunerStatus.Available) >= 0)
+                {
+                    return new MediaSourceInfo
+                    {
+                        Path = GetApiUrl(info) + "/auto/v" + channelId,
+                        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 = 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
+
+                            }
+                        }
+                    };
+                }
+
+                throw new ApplicationException("No tuners avaliable.");
+            } 
+            throw new ApplicationException("Channel not found.");
+        }
+    }
+}

+ 189 - 0
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -0,0 +1,189 @@
+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.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 : ITunerHost
+    {
+        public string Type
+        {
+            get { return "m3u"; }
+        }
+
+        public string Name
+        {
+            get { return "M3U Tuner"; }
+        }
+
+        private readonly IConfigurationManager _config;
+
+        public M3UTunerHost(IConfigurationManager config)
+        {
+            _config = config;
+        }
+
+        public Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo info, CancellationToken cancellationToken)
+        {
+            int position = 0;
+            string line;
+            // Read the file and display it line by line.
+            var file = new StreamReader(info.Url);
+            var channels = new List<M3UChannel>();
+            while ((line = file.ReadLine()) != null)
+            {
+                line = line.Trim();
+                if (!String.IsNullOrWhiteSpace(line))
+                {
+                    if (position == 0 && !line.StartsWith("#EXTM3U"))
+                    {
+                        throw new ApplicationException("wrong file");
+                    }
+                    if (position % 2 == 0)
+                    {
+                        if (position != 0)
+                        {
+                            channels.Last().Path = line;
+                        }
+                        else
+                        {
+                            line = line.Replace("#EXTM3U", "");
+                            line = line.Trim();
+                            var vars = line.Split(' ').ToList();
+                            foreach (var variable in vars)
+                            {
+                                var list = variable.Replace('"', ' ').Split('=');
+                                switch (list[0])
+                                {
+                                    case ("id"):
+                                        //_id = list[1];
+                                        break;
+                                }
+                            }
+                        }
+                    }
+                    else
+                    {
+                        if (!line.StartsWith("#EXTINF:")) { throw new ApplicationException("Bad file"); }
+                        line = line.Replace("#EXTINF:", "");
+                        var nameStart = line.LastIndexOf(',');
+                        line = line.Substring(0, nameStart);
+                        var vars = line.Split(' ').ToList();
+                        vars.RemoveAt(0);
+                        channels.Add(new M3UChannel());
+                        foreach (var variable in vars)
+                        {
+                            var list = variable.Replace('"', ' ').Split('=');
+                            switch (list[0])
+                            {
+                                case "tvg-id":
+                                    channels.Last().Id = list[1];
+                                    channels.Last().Number = list[1];
+                                    break;
+                                case "tvg-name":
+                                    channels.Last().Name = list[1];
+                                    break;
+                            }
+                        }
+                    }
+                    position++;
+                }
+            }
+            file.Close();
+            return Task.FromResult((IEnumerable<ChannelInfo>)channels);
+        }
+
+        public Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
+        {
+            var list = new List<LiveTvTunerInfo>();
+
+            list.Add(new LiveTvTunerInfo()
+            {
+                Name = Name,
+                SourceType = Type,
+                Status = LiveTvTunerStatus.Available,
+                Id = info.Url.GetMD5().ToString("N"),
+                Url = info.Url
+            });
+
+            return Task.FromResult(list);
+        }
+
+        private LiveTvOptions GetConfiguration()
+        {
+            return _config.GetConfiguration<LiveTvOptions>("livetv");
+        }
+
+        public List<TunerHostInfo> GetTunerHosts()
+        {
+            return GetConfiguration().TunerHosts.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)).ToList();
+        }
+
+        public async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
+        {
+            var channels = await GetChannels(info, cancellationToken).ConfigureAwait(false);
+            var m3uchannels = channels.Cast<M3UChannel>();
+            var channel = m3uchannels.FirstOrDefault(c => c.Id == channelId);
+            if (channel != null)
+            {
+                var path = channel.Path;
+                MediaProtocol protocol = MediaProtocol.File;
+                if (path.StartsWith("http"))
+                {
+                    protocol = MediaProtocol.Http;
+                }
+                else if (path.StartsWith("rtmp"))
+                {
+                    protocol = MediaProtocol.Rtmp;
+                }
+                else if (path.StartsWith("rtsp"))
+                {
+                    protocol = MediaProtocol.Rtsp;
+                }
+
+                return 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
+
+                        }
+                    }
+                };
+            }
+            throw new ApplicationException("Host doesnt provide this channel");
+        }
+
+        class M3UChannel : ChannelInfo
+        {
+            public string Path { get; set; }
+
+            public M3UChannel()
+            {
+            }
+        }
+    }
+}

+ 7 - 0
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -216,10 +216,17 @@
     <Compile Include="Library\Validators\StudiosValidator.cs" />
     <Compile Include="Library\Validators\YearsPostScanTask.cs" />
     <Compile Include="LiveTv\ChannelImageProvider.cs" />
+    <Compile Include="LiveTv\EmbyTV\EmbyTV.cs" />
+    <Compile Include="LiveTv\EmbyTV\ItemDataProvider.cs" />
+    <Compile Include="LiveTv\EmbyTV\RecordingHelper.cs" />
+    <Compile Include="LiveTv\EmbyTV\SeriesTimerManager.cs" />
+    <Compile Include="LiveTv\EmbyTV\TimerManager.cs" />
     <Compile Include="LiveTv\LiveTvConfigurationFactory.cs" />
     <Compile Include="LiveTv\LiveTvDtoService.cs" />
     <Compile Include="LiveTv\LiveTvManager.cs" />
     <Compile Include="LiveTv\LiveTvMediaSourceProvider.cs" />
+    <Compile Include="LiveTv\TunerHosts\HdHomerun.cs" />
+    <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
     <Compile Include="LiveTv\ProgramImageProvider.cs" />
     <Compile Include="LiveTv\RecordingImageProvider.cs" />
     <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />