diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs index 7ca6f43d6c..18441161f0 100644 --- a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs +++ b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs @@ -68,6 +68,9 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c ServicePointManager.Expect100Continue = false; + + // Trakt requests sometimes fail without this + ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls; } /// diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs index 75ca7e0dcc..beaa4eeafa 100644 --- a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs +++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs @@ -1,4 +1,5 @@ -using System; +using MediaBrowser.Model.LiveTv; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -7,6 +8,8 @@ namespace MediaBrowser.Controller.LiveTv { public interface IListingsProvider { - Task> GetProgramsAsync(ChannelInfo channel, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken); + string Name { get; } + Task> GetProgramsAsync(ListingsProviderInfo info, ChannelInfo channel, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken); + Task AddMetadata(ListingsProviderInfo info, List channels, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs index 2ca7397c1e..c78fbe630a 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs @@ -6,13 +6,16 @@ namespace MediaBrowser.Model.LiveTv { public int? GuideDays { get; set; } public bool EnableMovieProviders { get; set; } - public List TunerHosts { get; set; } public string RecordingPath { get; set; } + public List TunerHosts { get; set; } + public List ListingProviders { get; set; } + public LiveTvOptions() { EnableMovieProviders = true; TunerHosts = new List(); + ListingProviders = new List(); } } @@ -22,4 +25,13 @@ namespace MediaBrowser.Model.LiveTv public string Url { get; set; } public string Type { get; set; } } + + public class ListingsProviderInfo + { + public string ProviderName { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string ZipCode { get; set; } + public string ListingsId { get; set; } + } } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs index 18711c61e2..337e26e8d2 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelDownloadScheduledTask.cs @@ -1,8 +1,6 @@ using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Common.Security; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -29,22 +27,18 @@ namespace MediaBrowser.Server.Implementations.Channels private readonly IChannelManager _manager; private readonly IServerConfigurationManager _config; private readonly ILogger _logger; - private readonly IHttpClient _httpClient; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; - private readonly ISecurityManager _security; - public ChannelDownloadScheduledTask(IChannelManager manager, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, ILibraryManager libraryManager, IUserManager userManager, ISecurityManager security) + public ChannelDownloadScheduledTask(IChannelManager manager, IServerConfigurationManager config, ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager, IUserManager userManager) { _manager = manager; _config = config; _logger = logger; - _httpClient = httpClient; _fileSystem = fileSystem; _libraryManager = libraryManager; _userManager = userManager; - _security = security; } public string Name diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs index 0cd4b0a5c9..3e58e3bd56 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs @@ -23,7 +23,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -255,7 +254,7 @@ namespace MediaBrowser.Server.Implementations.Channels sources.InsertRange(0, cachedVersions); } - return sources.Where(IsValidMediaSource); + return sources; } public async Task> GetDynamicMediaSources(IChannelMediaItem item, CancellationToken cancellationToken) @@ -279,7 +278,6 @@ namespace MediaBrowser.Server.Implementations.Channels var list = SortMediaInfoResults(results) .Select(i => GetMediaSource(item, i)) - .Where(IsValidMediaSource) .ToList(); var cachedVersions = GetCachedChannelItemMediaSources(item); @@ -1424,18 +1422,8 @@ namespace MediaBrowser.Server.Implementations.Channels foreach (var source in list) { - try - { - await TryDownloadChannelItem(source, item, destination, progress, cancellationToken).ConfigureAwait(false); - return; - } - catch (HttpException ex) - { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - MarkBadMediaSource(source); - } - } + await TryDownloadChannelItem(source, item, destination, progress, cancellationToken).ConfigureAwait(false); + return; } } @@ -1525,81 +1513,6 @@ namespace MediaBrowser.Server.Implementations.Channels } } - private readonly ReaderWriterLockSlim _mediaSourceHistoryLock = new ReaderWriterLockSlim(); - private bool IsValidMediaSource(MediaSourceInfo source) - { - if (source.Protocol == MediaProtocol.Http) - { - return !GetBadMediaSourceHistory().Contains(source.Path, StringComparer.OrdinalIgnoreCase); - } - return true; - } - - private void MarkBadMediaSource(MediaSourceInfo source) - { - var list = GetBadMediaSourceHistory(); - list.Add(source.Path); - - var path = GetMediaSourceHistoryPath(); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - if (_mediaSourceHistoryLock.TryEnterWriteLock(TimeSpan.FromSeconds(5))) - { - try - { - File.WriteAllLines(path, list.ToArray(), Encoding.UTF8); - } - catch (Exception ex) - { - _logger.ErrorException("Error saving file", ex); - } - finally - { - _mediaSourceHistoryLock.ExitWriteLock(); - } - } - } - - private ConcurrentBag _badMediaSources = null; - private ConcurrentBag GetBadMediaSourceHistory() - { - if (_badMediaSources == null) - { - var path = GetMediaSourceHistoryPath(); - - if (_mediaSourceHistoryLock.TryEnterReadLock(TimeSpan.FromSeconds(1))) - { - if (_badMediaSources == null) - { - try - { - _badMediaSources = new ConcurrentBag(File.ReadAllLines(path, Encoding.UTF8)); - } - catch (IOException) - { - _badMediaSources = new ConcurrentBag(); - } - catch (Exception ex) - { - _logger.ErrorException("Error reading file", ex); - _badMediaSources = new ConcurrentBag(); - } - finally - { - _mediaSourceHistoryLock.ExitReadLock(); - } - } - } - } - return _badMediaSources; - } - - private string GetMediaSourceHistoryPath() - { - return Path.Combine(_config.ApplicationPaths.DataPath, "channels", "failures.txt"); - } - private void IncrementDownloadCount(string key, int? limit) { if (!limit.HasValue) diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs index 315493e0da..77dd54a80b 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs @@ -46,6 +46,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints data["guests"] = users.Count(i => i.ConnectLinkType.HasValue && i.ConnectLinkType.Value == UserLinkType.Guest).ToString(CultureInfo.InvariantCulture); data["linkedusers"] = users.Count(i => i.ConnectLinkType.HasValue && i.ConnectLinkType.Value == UserLinkType.LinkedUser).ToString(CultureInfo.InvariantCulture); + data["plugins"] = string.Join(",", _applicationHost.Plugins.Select(i => i.Id).ToArray()); + return _httpClient.Post(MbAdminUrl + "service/registration/ping", data, cancellationToken); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index b71d62f433..26e0709215 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -26,11 +26,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private readonly IConfigurationManager _config; private readonly IJsonSerializer _jsonSerializer; - private readonly List _tunerHosts = new List(); private readonly ItemDataProvider _recordingProvider; private readonly ItemDataProvider _seriesTimerProvider; private readonly TimerManager _timerProvider; + private readonly List _tunerHosts = new List(); + private readonly List _listingProviders = new List(); + public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IConfigurationManager config) { _appHpst = appHost; @@ -39,6 +41,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _config = config; _jsonSerializer = jsonSerializer; _tunerHosts.AddRange(appHost.GetExports()); + _listingProviders.AddRange(appHost.GetExports()); _recordingProvider = new ItemDataProvider(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")); @@ -118,6 +121,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } + if (list.Count > 0) + { + foreach (var provider in GetListingProviders()) + { + await provider.Item1.AddMetadata(provider.Item2, list, cancellationToken).ConfigureAwait(false); + } + } + return list; } @@ -246,9 +257,43 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return Task.FromResult((IEnumerable)_seriesTimerProvider.GetAll()); } - public Task> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + public async Task> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var allChannels = await GetChannelsAsync(cancellationToken).ConfigureAwait(false); + var channelInfo = allChannels.FirstOrDefault(i => string.Equals(channelId, i.Id, StringComparison.OrdinalIgnoreCase)); + + if (channelInfo == null) + { + _logger.Debug("Returning empty program list because channel was not found."); + return new List(); + } + + foreach (var provider in GetListingProviders()) + { + var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channelInfo, startDateUtc, endDateUtc, cancellationToken) + .ConfigureAwait(false); + var list = programs.ToList(); + + if (list.Count > 0) + { + return list; + } + } + + return new List(); + } + + private List> GetListingProviders() + { + return GetConfiguration().ListingProviders + .Select(i => + { + var provider = _listingProviders.FirstOrDefault(l => string.Equals(l.Name, i.ProviderName, StringComparison.OrdinalIgnoreCase)); + + return provider == null ? null : new Tuple(provider, i); + }) + .Where(i => i != null) + .ToList(); } public Task GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 7070c5a5fd..3b7564983e 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -1,6 +1,15 @@ -using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; +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; @@ -8,9 +17,830 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings { public class SchedulesDirect : IListingsProvider { - public Task> GetProgramsAsync(ChannelInfo channel, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClient _httpClient; + private const string UserAgent = "EmbyTV"; + private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); + + private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; + + private readonly ConcurrentDictionary _channelPair = + new ConcurrentDictionary(); + + public SchedulesDirect(ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient) + { + _logger = logger; + _jsonSerializer = jsonSerializer; + _httpClient = httpClient; + } + + public async Task> GetProgramsAsync(ListingsProviderInfo info, ChannelInfo channel, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + { + var channelNumber = channel.Number; + + List programsInfo = new List(); + + var token = await GetToken(info, cancellationToken); + + if (string.IsNullOrWhiteSpace(token)) + { + return programsInfo; + } + + if (string.IsNullOrWhiteSpace(info.ListingsId)) + { + return programsInfo; + } + + var httpOptions = new HttpRequestOptions() + { + Url = ApiUrl + "/schedules", + UserAgent = UserAgent, + CancellationToken = cancellationToken + }; + + httpOptions.RequestHeaders["token"] = token; + + List dates = new List(); + int numberOfDay = 0; + DateTime lastEntry = startDateUtc; + while (lastEntry != endDateUtc) + { + lastEntry = startDateUtc.AddDays(numberOfDay); + dates.Add(lastEntry.ToString("yyyy-MM-dd")); + numberOfDay++; + } + + ScheduleDirect.Station station = null; + + if (!_channelPair.TryGetValue("", out station)) + { + return programsInfo; + } + string stationID = station.stationID; + + _logger.Info("Channel Station ID is: " + stationID); + List requestList = + new List() + { + 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>(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 programsID = new List(); + 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>( + 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 channels, + CancellationToken cancellationToken) + { + var token = await GetToken(info, cancellationToken); + + _channelPair.Clear(); + + if (!String.IsNullOrWhiteSpace(token) && !String.IsNullOrWhiteSpace(info.ListingsId)) + { + 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(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 async Task GetLineup(string listingsId, string token, CancellationToken cancellationToken) + { + var httpOptions = new HttpRequestOptions() + { + Url = ApiUrl + "/lineups/" + listingsId, + UserAgent = UserAgent, + CancellationToken = cancellationToken + }; + + httpOptions.RequestHeaders["token"] = token; + + using (var response = await _httpClient.Get(httpOptions)) + { + var root = _jsonSerializer.DeserializeFromStream(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); + } + } + + return root; + } + } + + 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 hdtv = false; + bool repeat = (programInfo.@new == null); + string newID = programInfo.programID + "T" + startAt.Ticks + "C" + channel; + + + if (programInfo.audioProperties != null) + { + if (programInfo.audioProperties.Exists(item => item == "stereo")) + { + audioType = ProgramAudio.Stereo; + } + else + { + audioType = ProgramAudio.Mono; + } + } + + if ((programInfo.videoProperties != null)) + { + hdtv = programInfo.videoProperties.Exists(item => item == "hdtv"); + } + + string desc = ""; + if (details.descriptions != null) + { + if (details.descriptions.description1000 != null) + { + desc = details.descriptions.description1000[0].description; + } + else if (details.descriptions.description100 != null) + { + desc = details.descriptions.description100[0].description; + } + } + ScheduleDirect.Gracenote gracenote; + string EpisodeTitle = ""; + if (details.metadata != null) + { + gracenote = details.metadata.Find(x => x.Gracenote != null).Gracenote; + if ((details.showType ?? "No ShowType") == "Series") + { + EpisodeTitle = "Season: " + gracenote.season + " Episode: " + gracenote.episode; + } + } + if (details.episodeTitle150 != null) + { + EpisodeTitle = EpisodeTitle + " " + details.episodeTitle150; + } + bool hasImage = false; + var imageLink = ""; + + if (details.hasImageArtwork) + { + imageLink = details.images; + } + + + var info = new ProgramInfo + { + ChannelId = channel, + Id = newID, + Overview = desc, + StartDate = startAt, + EndDate = endAt, + Genres = new List() { "N/A" }, + Name = details.titles[0].title120 ?? "Unkown", + OfficialRating = "0", + CommunityRating = null, + EpisodeTitle = EpisodeTitle, + Audio = audioType, + IsHD = hdtv, + IsRepeat = repeat, + IsSeries = + ((details.showType ?? "No ShowType") == "Series") || + (details.showType ?? "No ShowType") == "Miniseries", + ImageUrl = imageLink, + HasImage = details.hasImageArtwork, + IsNews = false, + IsKids = false, + IsSports = + ((details.showType ?? "No ShowType") == "Sports non-event") || + (details.showType ?? "No ShowType") == "Sports event", + IsLive = false, + IsMovie = + (details.showType ?? "No ShowType") == "Feature Film" || + (details.showType ?? "No ShowType") == "TV Movie" || + (details.showType ?? "No ShowType") == "Short Film", + IsPremiere = false, + }; + //logger.Info("Done init"); + if (null != 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); + info.IsKids = false; + } + 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> GetImageForPrograms(List programIds, + CancellationToken cancellationToken) + { + var imageIdString = "["; + + programIds.ForEach(i => + { + if (!imageIdString.Contains(i.Substring(0, 10))) + { + imageIdString += "\"" + i.Substring(0, 10) + "\","; + } + ; + }); + imageIdString = imageIdString.TrimEnd(',') + "]"; + _logger.Debug("Json for show images = " + imageIdString); + var httpOptions = new HttpRequestOptions() + { + Url = ApiUrl + "/metadata/programs", + UserAgent = UserAgent, + CancellationToken = cancellationToken, + RequestContent = imageIdString + }; + List images; + using (var innerResponse2 = await _httpClient.Post(httpOptions)) + { + images = _jsonSerializer.DeserializeFromStream>( + innerResponse2.Content); + } + + return images; + } + + public async Task> GetHeadends(ListingsProviderInfo info, CancellationToken cancellationToken) + { + var token = await GetToken(info, cancellationToken); + + var lineups = new List(); + + if (string.IsNullOrWhiteSpace(token)) + { + return lineups; + } + + _logger.Info("Headends on account "); + + var options = new HttpRequestOptions() + { + Url = ApiUrl + "/headends?country=USA&postalcode=" + info.ZipCode, + UserAgent = UserAgent, + CancellationToken = cancellationToken + }; + + options.RequestHeaders["token"] = token; + + try + { + using (Stream responce = await _httpClient.Get(options).ConfigureAwait(false)) + { + var root = _jsonSerializer.DeserializeFromStream>(responce); + _logger.Info("Lineups on account "); + if (root != null) + { + foreach (ScheduleDirect.Headends headend in root) + { + _logger.Info("Headend: " + headend.headend); + foreach (ScheduleDirect.Lineup lineup in headend.lineups) + { + _logger.Info("Headend: " + lineup.uri.Substring(18)); + lineups.Add(new NameIdPair() + { + Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name, + Id = lineup.uri.Substring(18) + }); + } + } + } + else + { + _logger.Info("No lineups on account"); + } + } + } + catch (Exception ex) + { + _logger.Error("Error getting headends", ex); + } + + return lineups; + } + + private readonly ConcurrentDictionary _tokens = new ConcurrentDictionary(); + + private async Task GetToken(ListingsProviderInfo info, CancellationToken cancellationToken) + { + await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + 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; + } + + 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; + } + } + } + + var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false); + savedToken.Name = result; + savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture); + return result; + } + finally + { + _tokenSemaphore.Release(); + } + } + + private async Task GetTokenInternal(string username, string password, + CancellationToken cancellationToken) { - throw new NotImplementedException(); + 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(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); + } + } + + public string Name + { + get { return "Schedules Direct"; } + } + + 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 lineups { get; set; } + } + + + public class Headends + { + public string headend { get; set; } + public string transport { get; set; } + public string location { get; set; } + public List 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 broadcastLanguage { get; set; } + public List 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 { get; set; } + public List stations { get; set; } + public Metadata metadata { get; set; } + } + + public class RequestScheduleForChannel + { + public string stationID { get; set; } + public List 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 audioProperties { get; set; } + public List videoProperties { get; set; } + public List 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 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 { get; set; } + public List 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 { get; set; } + } + + public class Recommendation + { + public string programID { get; set; } + public string title120 { get; set; } + } + + public class ProgramDetails + { + public string programID { get; set; } + public List 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/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json index 89ff3a9844..d2b998ae8e 100644 --- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -817,5 +817,8 @@ "ButtonShare": "Share", "HeaderConfirm": "Confirm", "ButtonAdvancedRefresh": "Advanced Refresh", - "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?", + "HeaderDeleteProvider": "Delete Provider", + "HeaderAddProvider": "Add Provider" } diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json index b6c1321fa5..83c2ccf5d6 100644 --- a/MediaBrowser.Server.Implementations/Localization/Server/server.json +++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json @@ -695,7 +695,7 @@ "NotificationOptionNewLibraryContentMultiple": "New content added (multiple)", "NotificationOptionCameraImageUploaded": "Camera image uploaded", "NotificationOptionUserLockedOut": "User locked out", - "HeaderSendNotificationHelp": "By default, notifications are delivered to your dashboard inbox. Browse the plugin catalog to install additional notification options.", + "HeaderSendNotificationHelp": "Notifications are delivered to your Emby inbox. Additional options can be installed from the Services tab.", "NotificationOptionServerRestartRequired": "Server restart required", "LabelNotificationEnabled": "Enable this notification", "LabelMonitorUsers": "Monitor activity from:", @@ -1480,5 +1480,8 @@ "HeaderExternalServices": "External Services", "LabelIpAddressPath": "IP Address / Path:", "TabExternalServices": "External Services", - "TabTuners": "Tuners" + "TabTuners": "Tuners", + "HeaderGuideProviders": "Guide Providers", + "AddGuideProviderHelp": "Add a source for TV Guide information", + "LabelZipCode": "Zip Code" } diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index ef5bc71efd..1920a9ed79 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -183,6 +183,9 @@ <Content Include="dashboard-ui\livetvguide.html"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> + <Content Include="dashboard-ui\livetvguidesettings.html"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> <Content Include="dashboard-ui\livetvrecordings.html"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> @@ -204,6 +207,9 @@ <Content Include="dashboard-ui\scripts\homeupcoming.js"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> + <Content Include="dashboard-ui\scripts\livetvguidesettings.js"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> <Content Include="dashboard-ui\scripts\mypreferenceshome.js"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content>