|
@@ -6,9 +6,9 @@ using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.IO;
|
|
|
|
+using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Net.Http;
|
|
-using System.Security.Cryptography;
|
|
|
|
using System.Threading;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Extensions;
|
|
using Jellyfin.Extensions;
|
|
@@ -32,21 +32,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|
private readonly IServerConfigurationManager _config;
|
|
private readonly IServerConfigurationManager _config;
|
|
private readonly IHttpClientFactory _httpClientFactory;
|
|
private readonly IHttpClientFactory _httpClientFactory;
|
|
private readonly ILogger<XmlTvListingsProvider> _logger;
|
|
private readonly ILogger<XmlTvListingsProvider> _logger;
|
|
- private readonly IFileSystem _fileSystem;
|
|
|
|
- private readonly IZipClient _zipClient;
|
|
|
|
|
|
|
|
public XmlTvListingsProvider(
|
|
public XmlTvListingsProvider(
|
|
IServerConfigurationManager config,
|
|
IServerConfigurationManager config,
|
|
IHttpClientFactory httpClientFactory,
|
|
IHttpClientFactory httpClientFactory,
|
|
- ILogger<XmlTvListingsProvider> logger,
|
|
|
|
- IFileSystem fileSystem,
|
|
|
|
- IZipClient zipClient)
|
|
|
|
|
|
+ ILogger<XmlTvListingsProvider> logger)
|
|
{
|
|
{
|
|
_config = config;
|
|
_config = config;
|
|
_httpClientFactory = httpClientFactory;
|
|
_httpClientFactory = httpClientFactory;
|
|
_logger = logger;
|
|
_logger = logger;
|
|
- _fileSystem = fileSystem;
|
|
|
|
- _zipClient = zipClient;
|
|
|
|
}
|
|
}
|
|
|
|
|
|
public string Name => "XmlTV";
|
|
public string Name => "XmlTV";
|
|
@@ -67,16 +61,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|
{
|
|
{
|
|
_logger.LogInformation("xmltv path: {Path}", info.Path);
|
|
_logger.LogInformation("xmltv path: {Path}", info.Path);
|
|
|
|
|
|
- if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
|
|
|
- {
|
|
|
|
- return UnzipIfNeeded(info.Path, info.Path);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
string cacheFilename = info.Id + ".xml";
|
|
string cacheFilename = info.Id + ".xml";
|
|
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
|
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
|
|
|
+
|
|
if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
|
|
if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
|
|
{
|
|
{
|
|
- return UnzipIfNeeded(info.Path, cacheFile);
|
|
|
|
|
|
+ return cacheFile;
|
|
}
|
|
}
|
|
|
|
|
|
// Must check if file exists as parent directory may not exist.
|
|
// Must check if file exists as parent directory may not exist.
|
|
@@ -84,93 +74,48 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|
{
|
|
{
|
|
File.Delete(cacheFile);
|
|
File.Delete(cacheFile);
|
|
}
|
|
}
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
|
|
|
+ }
|
|
|
|
|
|
- _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
|
|
|
|
-
|
|
|
|
- Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
|
|
|
|
|
+ if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
|
|
|
+ {
|
|
|
|
+ _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
|
|
|
|
|
|
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
|
|
|
|
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
- await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous))
|
|
|
|
|
|
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
|
|
|
|
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
+ return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
|
|
|
|
+ }
|
|
|
|
+ else
|
|
{
|
|
{
|
|
- await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
+ await using var stream = AsyncFile.OpenRead(info.Path);
|
|
|
|
+ return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
-
|
|
|
|
- return UnzipIfNeeded(info.Path, cacheFile);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
- private string UnzipIfNeeded(ReadOnlySpan<char> originalUrl, string file)
|
|
|
|
|
|
+ private async Task<string> UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToken cancellationToken)
|
|
{
|
|
{
|
|
- ReadOnlySpan<char> ext = Path.GetExtension(originalUrl.LeftPart('?'));
|
|
|
|
|
|
+ await using var fileStream = new FileStream(file, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
|
|
|
|
|
- if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
+ if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
{
|
|
try
|
|
try
|
|
{
|
|
{
|
|
- string tempFolder = ExtractGz(file);
|
|
|
|
- return FindXmlFile(tempFolder);
|
|
|
|
- }
|
|
|
|
- catch (Exception ex)
|
|
|
|
- {
|
|
|
|
- _logger.LogError(ex, "Error extracting from gz file {File}", file);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- try
|
|
|
|
- {
|
|
|
|
- string tempFolder = ExtractFirstFileFromGz(file);
|
|
|
|
- return FindXmlFile(tempFolder);
|
|
|
|
|
|
+ using var reader = new GZipStream(stream, CompressionMode.Decompress);
|
|
|
|
+ await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
catch (Exception ex)
|
|
{
|
|
{
|
|
- _logger.LogError(ex, "Error extracting from zip file {File}", file);
|
|
|
|
|
|
+ _logger.LogError(ex, "Error extracting from gz file {File}", originalUrl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-
|
|
|
|
- return file;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private string ExtractFirstFileFromGz(string file)
|
|
|
|
- {
|
|
|
|
- using (var stream = File.OpenRead(file))
|
|
|
|
- {
|
|
|
|
- string tempFolder = GetTempFolderPath(stream);
|
|
|
|
- Directory.CreateDirectory(tempFolder);
|
|
|
|
-
|
|
|
|
- _zipClient.ExtractFirstFileFromGz(stream, tempFolder, "data.xml");
|
|
|
|
-
|
|
|
|
- return tempFolder;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private string ExtractGz(string file)
|
|
|
|
- {
|
|
|
|
- using (var stream = File.OpenRead(file))
|
|
|
|
|
|
+ else
|
|
{
|
|
{
|
|
- string tempFolder = GetTempFolderPath(stream);
|
|
|
|
- Directory.CreateDirectory(tempFolder);
|
|
|
|
-
|
|
|
|
- _zipClient.ExtractAllFromGz(stream, tempFolder, true);
|
|
|
|
-
|
|
|
|
- return tempFolder;
|
|
|
|
|
|
+ await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
- }
|
|
|
|
|
|
|
|
- private string GetTempFolderPath(Stream stream)
|
|
|
|
- {
|
|
|
|
-#pragma warning disable CA5351
|
|
|
|
- using var md5 = MD5.Create();
|
|
|
|
-#pragma warning restore CA5351
|
|
|
|
- var checksum = Convert.ToHexString(md5.ComputeHash(stream));
|
|
|
|
- stream.Position = 0;
|
|
|
|
- return Path.Combine(_config.ApplicationPaths.TempDirectory, checksum);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private string FindXmlFile(string directory)
|
|
|
|
- {
|
|
|
|
- return _fileSystem.GetFiles(directory, true)
|
|
|
|
- .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase))
|
|
|
|
- .Select(i => i.FullName)
|
|
|
|
- .FirstOrDefault();
|
|
|
|
|
|
+ return file;
|
|
}
|
|
}
|
|
|
|
|
|
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
|
|
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
|
|
@@ -213,16 +158,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|
IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
|
IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
|
IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
|
IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
|
IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
|
IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.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,
|
|
|
|
|
|
+ ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
|
|
|
|
+ HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
|
|
|
|
+ OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
|
|
CommunityRating = program.StarRating,
|
|
CommunityRating = program.StarRating,
|
|
- SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture)
|
|
|
|
|
|
+ SeriesId = program.Episode == null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
|
|
};
|
|
};
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(program.ProgramId))
|
|
if (string.IsNullOrWhiteSpace(program.ProgramId))
|
|
{
|
|
{
|
|
- string uniqueString = (program.Title ?? string.Empty) + (episodeTitle ?? string.Empty) /*+ (p.IceTvEpisodeNumber ?? string.Empty)*/;
|
|
|
|
|
|
+ string uniqueString = (program.Title ?? string.Empty) + (episodeTitle ?? string.Empty);
|
|
|
|
|
|
if (programInfo.SeasonNumber.HasValue)
|
|
if (programInfo.SeasonNumber.HasValue)
|
|
{
|
|
{
|