diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 62d1eb57ce..042a19d650 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -530,8 +530,8 @@ namespace Emby.Dlna }; } } - - class DlnaProfileEntryPoint /*: IServerEntryPoint*/ + /* + class DlnaProfileEntryPoint : IServerEntryPoint { private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; @@ -551,7 +551,7 @@ namespace Emby.Dlna private void DumpProfiles() { - var list = new List + DeviceProfile[] list = new [] { new SamsungSmartTvProfile(), new XboxOneProfile(), @@ -596,5 +596,6 @@ namespace Emby.Dlna public void Dispose() { } - } -} \ No newline at end of file + }*/ +} + diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs index 0208183614..9d1b751d75 100644 --- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs @@ -3,6 +3,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Progress; using MediaBrowser.Model.Tasks; @@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Channels get { return true; } } - public async Task Execute(System.Threading.CancellationToken cancellationToken, IProgress progress) + public async Task Execute(CancellationToken cancellationToken, IProgress progress) { var manager = (ChannelManager)_channelManager; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 9021666a30..2d0eaf25ba 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -1298,4 +1298,4 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } -} \ No newline at end of file +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs new file mode 100644 index 0000000000..a8609ce2da --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Emby.XmlTv.Classes; +using Emby.XmlTv.Entities; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; + +namespace Jellyfin.Server.Implementations.LiveTv.Listings +{ + public class XmlTvListingsProvider : IListingsProvider + { + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IZipClient _zipClient; + + public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger, IFileSystem fileSystem, IZipClient zipClient) + { + _config = config; + _httpClient = httpClient; + _logger = logger; + _fileSystem = fileSystem; + _zipClient = zipClient; + } + + public string Name + { + get { return "XmlTV"; } + } + + public string Type + { + get { return "xmltv"; } + } + + private string GetLanguage(ListingsProviderInfo info) + { + if (!string.IsNullOrWhiteSpace(info.PreferredLanguage)) + { + return info.PreferredLanguage; + } + + return _config.Configuration.PreferredMetadataLanguage; + } + + private async Task GetXml(string path, CancellationToken cancellationToken) + { + _logger.Info("xmltv path: {0}", path); + + if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return UnzipIfNeeded(path, path); + } + + string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml"; + string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename); + if (_fileSystem.FileExists(cacheFile)) + { + return UnzipIfNeeded(path, cacheFile); + } + + _logger.Info("Downloading xmltv listings from {0}", path); + + string tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = path, + Progress = new SimpleProgress(), + DecompressionMethod = CompressionMethod.Gzip, + + // It's going to come back gzipped regardless of this value + // So we need to make sure the decompression method is set to gzip + EnableHttpCompression = true, + + UserAgent = "Emby/3.0" + + }).ConfigureAwait(false); + + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(cacheFile)); + + _fileSystem.CopyFile(tempFile, cacheFile, true); + + return UnzipIfNeeded(path, cacheFile); + } + + private string UnzipIfNeeded(string originalUrl, string file) + { + string ext = Path.GetExtension(originalUrl.Split('?')[0]); + + if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase)) + { + try + { + string tempFolder = ExtractGz(file); + return FindXmlFile(tempFolder); + } + catch (Exception ex) + { + _logger.ErrorException("Error extracting from gz file {0}", ex, file); + } + + try + { + string tempFolder = ExtractFirstFileFromGz(file); + return FindXmlFile(tempFolder); + } + catch (Exception ex) + { + _logger.ErrorException("Error extracting from zip file {0}", ex, file); + } + } + + return file; + } + + private string ExtractFirstFileFromGz(string file) + { + using (var stream = _fileSystem.OpenRead(file)) + { + string tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString()); + _fileSystem.CreateDirectory(tempFolder); + + _zipClient.ExtractFirstFileFromGz(stream, tempFolder, "data.xml"); + + return tempFolder; + } + } + + private string ExtractGz(string file) + { + using (var stream = _fileSystem.OpenRead(file)) + { + string tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString()); + _fileSystem.CreateDirectory(tempFolder); + + _zipClient.ExtractAllFromGz(stream, tempFolder, true); + + return tempFolder; + } + } + + private string FindXmlFile(string directory) + { + return _fileSystem.GetFiles(directory, true) + .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase)) + .Select(i => i.FullName) + .FirstOrDefault(); + } + + public async Task> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(channelId)) + { + throw new ArgumentNullException("channelId"); + } + + /* + if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false)) + { + var length = endDateUtc - startDateUtc; + if (length.TotalDays > 1) + { + endDateUtc = startDateUtc.AddDays(1); + } + }*/ + + _logger.Debug("Getting xmltv programs for channel {0}", channelId); + + string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); + _logger.Debug("Opening XmlTvReader for {0}", path); + var reader = new XmlTvReader(path, GetLanguage(info)); + + return reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken) + .Select(p => GetProgramInfo(p, info)); + } + + private ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) + { + string episodeTitle = program.Episode?.Title; + + var programInfo = new ProgramInfo + { + ChannelId = program.ChannelId, + EndDate = program.EndDate.UtcDateTime, + EpisodeNumber = program.Episode?.Episode, + EpisodeTitle = episodeTitle, + Genres = program.Categories, + StartDate = program.StartDate.UtcDateTime, + Name = program.Title, + Overview = program.Description, + ProductionYear = program.CopyrightDate?.Year, + SeasonNumber = program.Episode?.Series, + IsSeries = program.Episode != null, + IsRepeat = program.IsPreviouslyShown && !program.IsNew, + IsPremiere = program.Premiere != null, + IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), + IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), + IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), + IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), + ImageUrl = program.Icon != null && !String.IsNullOrEmpty(program.Icon.Source) ? program.Icon.Source : null, + HasImage = program.Icon != null && !String.IsNullOrEmpty(program.Icon.Source), + OfficialRating = program.Rating != null && !String.IsNullOrEmpty(program.Rating.Value) ? program.Rating.Value : null, + CommunityRating = program.StarRating, + SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N") + }; + + if (string.IsNullOrWhiteSpace(program.ProgramId)) + { + string uniqueString = (program.Title ?? string.Empty) + (episodeTitle ?? string.Empty) /*+ (p.IceTvEpisodeNumber ?? string.Empty)*/; + + if (programInfo.SeasonNumber.HasValue) + { + uniqueString = "-" + programInfo.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture); + } + if (programInfo.EpisodeNumber.HasValue) + { + uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture); + } + + programInfo.ShowId = uniqueString.GetMD5().ToString("N"); + + // If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skipped + if (programInfo.IsSeries + && !programInfo.IsRepeat + && (programInfo.EpisodeNumber ?? 0) == 0) + { + programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); + } + } + else + { + programInfo.ShowId = program.ProgramId; + } + + // Construct an id from the channel and start date + programInfo.Id = String.Format("{0}_{1:O}", program.ChannelId, program.StartDate); + + if (programInfo.IsMovie) + { + programInfo.IsSeries = false; + programInfo.EpisodeNumber = null; + programInfo.EpisodeTitle = null; + } + + return programInfo; + } + + public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings) + { + // Assume all urls are valid. check files for existence + if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !_fileSystem.FileExists(info.Path)) + { + throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path); + } + + return Task.CompletedTask; + } + + public async Task> GetLineups(ListingsProviderInfo info, string country, string location) + { + // In theory this should never be called because there is always only one lineup + string path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false); + _logger.Debug("Opening XmlTvReader for {0}", path); + var reader = new XmlTvReader(path, GetLanguage(info)); + IEnumerable results = reader.GetChannels(); + + // Should this method be async? + return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList(); + } + + public async Task> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) + { + // In theory this should never be called because there is always only one lineup + string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); + _logger.Debug("Opening XmlTvReader for {0}", path); + var reader = new XmlTvReader(path, GetLanguage(info)); + IEnumerable results = reader.GetChannels(); + + // Should this method be async? + return results.Select(c => new ChannelInfo + { + Id = c.Id, + Name = c.DisplayName, + ImageUrl = c.Icon != null && !String.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null, + Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number + + }).ToList(); + } + } +} diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 2e96796784..e4400220e6 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -2392,21 +2392,25 @@ namespace Emby.Server.Implementations.LiveTv public async Task SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings) { + // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider + // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider info = _jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info)); - var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); + IListingsProvider provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); if (provider == null) { - throw new ResourceNotFoundException(); + throw new ResourceNotFoundException( + string.Format("Couldn't find provider of type: '{0}'", info.Type) + ); } await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false); - var config = GetConfiguration(); + LiveTvOptions config = GetConfiguration(); - var list = config.ListingProviders.ToList(); - var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); + List list = config.ListingProviders.ToList(); + int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) {