浏览代码

update tuner setup

Luke Pulverenti 10 年之前
父节点
当前提交
3fda8ec5c2

+ 48 - 8
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Configuration;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
@@ -345,6 +346,31 @@ namespace MediaBrowser.Api.LiveTv
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
 
 
+    [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")]
+    [Authenticated]
+    public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
+    {
+    }
+
+    [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")]
+    [Authenticated]
+    public class DeleteListingProvider : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
+        public string Id { get; set; }
+    }
+
+    [Route("/LiveTv/ListingProviders/Lineups", "GET", Summary = "Gets available lineups")]
+    [Authenticated]
+    public class GetLineups : IReturn<List<NameIdPair>>
+    {
+        [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "Location", Description = "Location", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string Location { get; set; }
+    }
+
     public class LiveTvService : BaseApiService
     public class LiveTvService : BaseApiService
     {
     {
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILiveTvManager _liveTvManager;
@@ -373,20 +399,27 @@ namespace MediaBrowser.Api.LiveTv
             }
             }
         }
         }
 
 
-        public void Post(AddTunerHost request)
+        public object Post(AddListingProvider request)
+        {
+            var result = _liveTvManager.SaveListingProvider(request).Result;
+            return ToOptimizedResult(result);
+        }
+
+        public void Delete(DeleteListingProvider request)
         {
         {
             var config = GetConfiguration();
             var config = GetConfiguration();
 
 
-            config.TunerHosts.Add(new TunerHostInfo
-            {
-                Id = Guid.NewGuid().ToString("N"),
-                Url = request.Url,
-                Type = request.Type
-            });
+            config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(request.Id, i.Id, StringComparison.OrdinalIgnoreCase)).ToList();
 
 
             _config.SaveConfiguration("livetv", config);
             _config.SaveConfiguration("livetv", config);
         }
         }
 
 
+        public void Post(AddTunerHost request)
+        {
+            var task = _liveTvManager.SaveTunerHost(request);
+            Task.WaitAll(task);
+        }
+
         public void Delete(DeleteTunerHost request)
         public void Delete(DeleteTunerHost request)
         {
         {
             var config = GetConfiguration();
             var config = GetConfiguration();
@@ -401,6 +434,13 @@ namespace MediaBrowser.Api.LiveTv
             return _config.GetConfiguration<LiveTvOptions>("livetv");
             return _config.GetConfiguration<LiveTvOptions>("livetv");
         }
         }
 
 
+        public async Task<object> Get(GetLineups request)
+        {
+            var info = await _liveTvManager.GetLineups(request.Id, request.Location).ConfigureAwait(false);
+
+            return ToOptimizedSerializedResultUsingCache(info);
+        }
+
         public async Task<object> Get(GetLiveTvInfo request)
         public async Task<object> Get(GetLiveTvInfo request)
         {
         {
             var info = await _liveTvManager.GetLiveTvInfo(CancellationToken.None).ConfigureAwait(false);
             var info = await _liveTvManager.GetLiveTvInfo(CancellationToken.None).ConfigureAwait(false);

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

@@ -1,4 +1,5 @@
-using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading;
@@ -9,7 +10,10 @@ namespace MediaBrowser.Controller.LiveTv
     public interface IListingsProvider
     public interface IListingsProvider
     {
     {
         string Name { get; }
         string Name { get; }
+        string Type { get; }
         Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, ChannelInfo channel, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken);
         Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, ChannelInfo channel, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken);
         Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken);
         Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken);
+        Task Validate(ListingsProviderInfo info);
+        Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string location);
     }
     }
 }
 }

+ 23 - 2
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -56,12 +56,14 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="id">The identifier.</param>
         /// <param name="id">The identifier.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         Task CancelSeriesTimer(string id);
         Task CancelSeriesTimer(string id);
-        
+
         /// <summary>
         /// <summary>
         /// Adds the parts.
         /// Adds the parts.
         /// </summary>
         /// </summary>
         /// <param name="services">The services.</param>
         /// <param name="services">The services.</param>
-        void AddParts(IEnumerable<ILiveTvService> services);
+        /// <param name="tunerHosts">The tuner hosts.</param>
+        /// <param name="listingProviders">The listing providers.</param>
+        void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders);
 
 
         /// <summary>
         /// <summary>
         /// Gets the channels.
         /// Gets the channels.
@@ -337,5 +339,24 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="dto">The dto.</param>
         /// <param name="dto">The dto.</param>
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
         void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, User user = null);
         void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, User user = null);
+        /// <summary>
+        /// Saves the tuner host.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <returns>Task.</returns>
+        Task SaveTunerHost(TunerHostInfo info);
+        /// <summary>
+        /// Saves the listing provider.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <returns>Task.</returns>
+        Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info);
+        /// <summary>
+        /// Gets the lineups.
+        /// </summary>
+        /// <param name="providerId">The provider identifier.</param>
+        /// <param name="location">The location.</param>
+        /// <returns>Task&lt;List&lt;NameIdPair&gt;&gt;.</returns>
+        Task<List<NameIdPair>> GetLineups(string providerId, string location);
     }
     }
 }
 }

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

@@ -46,5 +46,11 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
         /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
         Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken);
         Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken);
+        /// <summary>
+        /// Validates the specified information.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <returns>Task.</returns>
+        Task Validate(TunerHostInfo info);
     }
     }
 }
 }

+ 3 - 2
MediaBrowser.Model/LiveTv/LiveTvOptions.cs

@@ -28,10 +28,11 @@ namespace MediaBrowser.Model.LiveTv
 
 
     public class ListingsProviderInfo
     public class ListingsProviderInfo
     {
     {
-        public string ProviderName { get; set; }
+        public string Id { get; set; }
+        public string Type { get; set; }
         public string Username { get; set; }
         public string Username { get; set; }
         public string Password { get; set; }
         public string Password { get; set; }
-        public string ZipCode { get; set; }
         public string ListingsId { get; set; }
         public string ListingsId { get; set; }
+        public string ZipCode { get; set; }
     }
     }
 }
 }

+ 6 - 8
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -30,18 +30,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
         private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
         private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
         private readonly TimerManager _timerProvider;
         private readonly TimerManager _timerProvider;
 
 
-        private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>();
-        private readonly List<IListingsProvider> _listingProviders = new List<IListingsProvider>();
+        private readonly LiveTvManager _liveTvManager;
 
 
-        public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IConfigurationManager config)
+        public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IConfigurationManager config, ILiveTvManager liveTvManager)
         {
         {
             _appHpst = appHost;
             _appHpst = appHost;
             _logger = logger;
             _logger = logger;
             _httpClient = httpClient;
             _httpClient = httpClient;
             _config = config;
             _config = config;
+            _liveTvManager = (LiveTvManager)liveTvManager;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
-            _tunerHosts.AddRange(appHost.GetExports<ITunerHost>());
-            _listingProviders.AddRange(appHost.GetExports<IListingsProvider>());
 
 
             _recordingProvider = new ItemDataProvider<RecordingInfo>(jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase));
             _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"));
             _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
@@ -76,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             var status = new LiveTvServiceStatusInfo();
             var status = new LiveTvServiceStatusInfo();
             var list = new List<LiveTvTunerInfo>();
             var list = new List<LiveTvTunerInfo>();
 
 
-            foreach (var host in _tunerHosts)
+            foreach (var host in _liveTvManager.TunerHosts)
             {
             {
                 foreach (var hostInstance in host.GetTunerHosts())
                 foreach (var hostInstance in host.GetTunerHosts())
                 {
                 {
@@ -104,7 +102,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
         {
         {
             var list = new List<ChannelInfo>();
             var list = new List<ChannelInfo>();
 
 
-            foreach (var host in _tunerHosts)
+            foreach (var host in _liveTvManager.TunerHosts)
             {
             {
                 foreach (var hostInstance in host.GetTunerHosts())
                 foreach (var hostInstance in host.GetTunerHosts())
                 {
                 {
@@ -288,7 +286,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             return GetConfiguration().ListingProviders
             return GetConfiguration().ListingProviders
                 .Select(i =>
                 .Select(i =>
                 {
                 {
-                    var provider = _listingProviders.FirstOrDefault(l => string.Equals(l.Name, i.ProviderName, StringComparison.OrdinalIgnoreCase));
+                    var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
 
 
                     return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
                     return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
                 })
                 })

+ 43 - 30
MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -422,7 +422,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
             return images;
             return images;
         }
         }
 
 
-        public async Task<List<NameIdPair>> GetHeadends(ListingsProviderInfo info, CancellationToken cancellationToken)
+        public async Task<List<NameIdPair>> GetHeadends(ListingsProviderInfo info, string location, CancellationToken cancellationToken)
         {
         {
             var token = await GetToken(info, cancellationToken);
             var token = await GetToken(info, cancellationToken);
 
 
@@ -437,7 +437,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
 
 
             var options = new HttpRequestOptions()
             var options = new HttpRequestOptions()
             {
             {
-                Url = ApiUrl + "/headends?country=USA&postalcode=" + info.ZipCode,
+                Url = ApiUrl + "/headends?country=USA&postalcode=" + location,
                 UserAgent = UserAgent,
                 UserAgent = UserAgent,
                 CancellationToken = cancellationToken
                 CancellationToken = cancellationToken
             };
             };
@@ -484,43 +484,43 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
 
 
         private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken)
         private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken)
         {
         {
-            await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-            try
-            {
-                var username = info.Username;
+            var username = info.Username;
 
 
-                // Reset the token if there's no username
-                if (string.IsNullOrWhiteSpace(username))
-                {
-                    return null;
-                }
+            // Reset the token if there's no username
+            if (string.IsNullOrWhiteSpace(username))
+            {
+                return null;
+            }
 
 
-                var password = info.Password;
-                if (string.IsNullOrWhiteSpace(password))
-                {
-                    return null;
-                }
+            var password = info.Password;
+            if (string.IsNullOrWhiteSpace(password))
+            {
+                return null;
+            }
 
 
-                NameValuePair savedToken = null;
-                if (!_tokens.TryGetValue(username, out savedToken))
-                {
-                    savedToken = new NameValuePair();
-                    _tokens.TryAdd(username, savedToken);
-                }
+            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))
+            if (!string.IsNullOrWhiteSpace(savedToken.Name) && !string.IsNullOrWhiteSpace(savedToken.Value))
+            {
+                long ticks;
+                if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out ticks))
                 {
                 {
-                    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)
                     {
                     {
-                        // If it's under 24 hours old we can still use it
-                        if ((DateTime.UtcNow.Ticks - ticks) < TimeSpan.FromHours(24).Ticks)
-                        {
-                            return savedToken.Name;
-                        }
+                        return savedToken.Name;
                     }
                     }
                 }
                 }
+            }
 
 
+            await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+            try
+            {
                 var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
                 var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
                 savedToken.Name = result;
                 savedToken.Name = result;
                 savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
                 savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
@@ -563,6 +563,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
             get { return "Schedules Direct"; }
             get { return "Schedules Direct"; }
         }
         }
 
 
+        public string Type
+        {
+            get { return "SchedulesDirect"; }
+        }
+
         public class ScheduleDirect
         public class ScheduleDirect
         {
         {
             public class Token
             public class Token
@@ -842,5 +847,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
 
 
         }
         }
 
 
+        public async Task Validate(ListingsProviderInfo info)
+        {
+        }
+
+        public Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string location)
+        {
+            return GetHeadends(info, location, CancellationToken.None);
+        }
     }
     }
 }
 }

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

@@ -57,6 +57,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
         private readonly ConcurrentDictionary<Guid, Guid> _refreshedPrograms = new ConcurrentDictionary<Guid, Guid>();
         private readonly ConcurrentDictionary<Guid, Guid> _refreshedPrograms = new ConcurrentDictionary<Guid, Guid>();
 
 
+        private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>();
+        private readonly List<IListingsProvider> _listingProviders = new List<IListingsProvider>();
+
         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)
         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)
         {
         {
             _config = config;
             _config = config;
@@ -92,9 +95,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         /// Adds the parts.
         /// Adds the parts.
         /// </summary>
         /// </summary>
         /// <param name="services">The services.</param>
         /// <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);
             _services.AddRange(services);
+            _tunerHosts.AddRange(tunerHosts);
+            _listingProviders.AddRange(listingProviders);
 
 
             foreach (var service in _services)
             foreach (var service in _services)
             {
             {
@@ -102,6 +109,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)
         void service_DataSourceChanged(object sender, EventArgs e)
         {
         {
             _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
             _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
@@ -2154,5 +2171,84 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             var user = _userManager.GetUserById(userId);
             var user = _userManager.GetUserById(userId);
             return await _libraryManager.GetNamedView(user, name, "livetv", "zz_" + name, cancellationToken).ConfigureAwait(false);
             return await _libraryManager.GetNamedView(user, name, "livetv", "zz_" + name, cancellationToken).ConfigureAwait(false);
         }
         }
+
+        public async Task 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);
+        }
+
+        public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info)
+        {
+            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).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);
+
+            return info;
+        }
+
+
+        public Task<List<NameIdPair>> GetLineups(string providerId, string location)
+        {
+            var config = GetConfiguration();
+
+            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, location);
+        }
     }
     }
 }
 }

+ 6 - 2
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun.cs

@@ -3,7 +3,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
@@ -15,7 +14,6 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
 
 
 namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 {
 {
@@ -222,5 +220,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
             } 
             } 
             throw new ApplicationException("Channel not found.");
             throw new ApplicationException("Channel not found.");
         }
         }
+
+
+        public async Task Validate(TunerHostInfo info)
+        {
+            await GetChannels(info, CancellationToken.None).ConfigureAwait(false);
+        }
     }
     }
 }
 }

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

@@ -185,5 +185,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
             {
             {
             }
             }
         }
         }
+
+        public async Task Validate(TunerHostInfo info)
+        {
+            if (!File.Exists(info.Url))
+            {
+                throw new FileNotFoundException();
+            }
+        }
     }
     }
 }
 }

+ 4 - 1
MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json

@@ -820,5 +820,8 @@
     "MessageConfirmDeleteTunerDevice": "Are you sure you wish to delete this device?",
     "MessageConfirmDeleteTunerDevice": "Are you sure you wish to delete this device?",
     "MessageConfirmDeleteGuideProvider": "Are you sure you wish to delete this guide provider?",
     "MessageConfirmDeleteGuideProvider": "Are you sure you wish to delete this guide provider?",
     "HeaderDeleteProvider": "Delete Provider",
     "HeaderDeleteProvider": "Delete Provider",
-    "HeaderAddProvider": "Add Provider"
+    "HeaderAddProvider": "Add Provider",
+    "ErrorAddingTunerDevice": "There was an error adding the tuner device. Please ensure it is accessible and try again.",
+    "ErrorSavingTvProvider": "There was an error saving the TV provider. Please ensure it is accessible and try again.",
+    "ErrorGettingTvLineups": "There was an error downloading tv lineups. Please ensure your username and password are correct and try again."
 }
 }

+ 4 - 1
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -1483,5 +1483,8 @@
     "TabTuners": "Tuners",
     "TabTuners": "Tuners",
     "HeaderGuideProviders": "Guide Providers",
     "HeaderGuideProviders": "Guide Providers",
     "AddGuideProviderHelp": "Add a source for TV Guide information",
     "AddGuideProviderHelp": "Add a source for TV Guide information",
-    "LabelZipCode": "Zip Code"
+    "LabelZipCode": "Zip Code:",
+    "GuideProviderListingsStep": "Step 2: Select Listings",
+    "GuideProviderLoginStep": "Step 1: Login",
+    "LabelLineup": "Lineup"
 }
 }

+ 1 - 1
MediaBrowser.Server.Startup.Common/ApplicationHost.cs

@@ -784,7 +784,7 @@ namespace MediaBrowser.Server.Startup.Common
 
 
             ImageProcessor.AddParts(GetExports<IImageEnhancer>());
             ImageProcessor.AddParts(GetExports<IImageEnhancer>());
 
 
-            LiveTvManager.AddParts(GetExports<ILiveTvService>());
+            LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
 
 
             SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
             SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
             ChapterManager.AddParts(GetExports<IChapterProvider>());
             ChapterManager.AddParts(GetExports<IChapterProvider>());

+ 2 - 2
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -183,7 +183,7 @@
     <Content Include="dashboard-ui\livetvguide.html">
     <Content Include="dashboard-ui\livetvguide.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
-    <Content Include="dashboard-ui\livetvguidesettings.html">
+    <Content Include="dashboard-ui\livetvguideprovider-scd.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
     <Content Include="dashboard-ui\livetvrecordings.html">
     <Content Include="dashboard-ui\livetvrecordings.html">
@@ -207,7 +207,7 @@
     <Content Include="dashboard-ui\scripts\homeupcoming.js">
     <Content Include="dashboard-ui\scripts\homeupcoming.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
-    <Content Include="dashboard-ui\scripts\livetvguidesettings.js">
+    <Content Include="dashboard-ui\scripts\livetvguideprovider-scd.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
     <Content Include="dashboard-ui\scripts\mypreferenceshome.js">
     <Content Include="dashboard-ui\scripts\mypreferenceshome.js">