mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-27 19:08:27 +01:00
Merge branch 'master' into TVFix
This commit is contained in:
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@@ -45,21 +46,27 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
|
||||
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
|
||||
{
|
||||
onStarted();
|
||||
|
||||
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
|
||||
_logger.LogInformation("Copying recording to file {FilePath}", targetFile);
|
||||
|
||||
// The media source is infinite so we need to handle stopping ourselves
|
||||
using var durationToken = new CancellationTokenSource(duration);
|
||||
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
|
||||
var linkedCancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await using var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream());
|
||||
await _streamHelper.CopyToAsync(
|
||||
fileStream,
|
||||
output,
|
||||
IODefaults.CopyToBufferSize,
|
||||
1000,
|
||||
linkedCancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Recording completed to file {0}", targetFile);
|
||||
_logger.LogInformation("Recording completed: {FilePath}", targetFile);
|
||||
}
|
||||
|
||||
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
@@ -71,8 +78,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
|
||||
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
await using var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
|
||||
|
||||
onStarted();
|
||||
|
||||
|
||||
@@ -1848,14 +1848,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
return;
|
||||
}
|
||||
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
Encoding = Encoding.UTF8,
|
||||
CloseOutput = false
|
||||
Encoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
using (var writer = XmlWriter.Create(stream, settings))
|
||||
@@ -1913,14 +1911,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
return;
|
||||
}
|
||||
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
Encoding = Encoding.UTF8,
|
||||
CloseOutput = false
|
||||
Encoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
var options = _config.GetNfoConfiguration();
|
||||
@@ -1990,7 +1986,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
writer.WriteElementString(
|
||||
"dateadded",
|
||||
DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
|
||||
DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
|
||||
|
||||
if (item.ProductionYear.HasValue)
|
||||
{
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
|
||||
|
||||
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
||||
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
|
||||
_logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
|
||||
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
|
||||
@@ -188,7 +188,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
|
||||
inputTempFile,
|
||||
targetFile,
|
||||
targetFile.Replace("\"", "\\\""), // Escape quotes in filename
|
||||
videoArgs,
|
||||
GetAudioArgs(mediaSource),
|
||||
subtitleArgs,
|
||||
@@ -205,9 +205,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
// var audioChannels = 2;
|
||||
// var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
|
||||
// if (audioStream != null)
|
||||
//{
|
||||
// {
|
||||
// audioChannels = audioStream.Channels ?? audioChannels;
|
||||
//}
|
||||
// }
|
||||
// return "-codec:a:0 aac -strict experimental -ab 320000";
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
/// <summary>
|
||||
/// Records the specified media source.
|
||||
/// </summary>
|
||||
/// <param name="directStreamProvider">The direct stream provider, or <c>null</c>.</param>
|
||||
/// <param name="mediaSource">The media source.</param>
|
||||
/// <param name="targetFile">The target file.</param>
|
||||
/// <param name="duration">The duration to record.</param>
|
||||
/// <param name="onStarted">An action to perform when recording starts.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A <see cref="Task"/> that represents the recording operation.</returns>
|
||||
Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
|
||||
|
||||
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
@@ -18,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
private readonly string _dataPath;
|
||||
private readonly object _fileDataLock = new object();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private T[] _items;
|
||||
private T[]? _items;
|
||||
|
||||
public ItemDataProvider(
|
||||
ILogger logger,
|
||||
@@ -34,6 +33,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
protected Func<T, T, bool> EqualityComparer { get; }
|
||||
|
||||
[MemberNotNull(nameof(_items))]
|
||||
private void EnsureLoaded()
|
||||
{
|
||||
if (_items != null)
|
||||
@@ -49,6 +49,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
var bytes = File.ReadAllBytes(_dataPath);
|
||||
_items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions);
|
||||
if (_items == null)
|
||||
{
|
||||
Logger.LogError("Error deserializing {Path}, data was null", _dataPath);
|
||||
_items = Array.Empty<T>();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
@@ -62,7 +68,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
private void SaveList()
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath)));
|
||||
var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
|
||||
File.WriteAllText(_dataPath, jsonString);
|
||||
}
|
||||
|
||||
@@ -9,14 +9,15 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
@@ -34,7 +35,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
private readonly ILogger<SchedulesDirect> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly IApplicationHost _appHost;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
|
||||
@@ -44,12 +44,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
public SchedulesDirect(
|
||||
ILogger<SchedulesDirect> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IApplicationHost appHost,
|
||||
ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_appHost = appHost;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
}
|
||||
|
||||
@@ -114,18 +112,29 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
options.Headers.TryAddWithoutValidation("token", token);
|
||||
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var dailySchedules = await JsonSerializer.DeserializeAsync<List<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (dailySchedules == null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
}
|
||||
|
||||
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
|
||||
|
||||
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
|
||||
programRequestOptions.Headers.TryAddWithoutValidation("token", token);
|
||||
|
||||
var programsID = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
||||
programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
||||
programRequestOptions.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(programIds, _jsonOptions));
|
||||
programRequestOptions.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
|
||||
|
||||
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var programDetails = await JsonSerializer.DeserializeAsync<List<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (programDetails == null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
}
|
||||
|
||||
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
|
||||
|
||||
var programIdsWithImages = programDetails
|
||||
@@ -142,6 +151,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
// schedule.ProgramId + " which says it has images? " +
|
||||
// programDict[schedule.ProgramId].hasImageArtwork);
|
||||
|
||||
if (string.IsNullOrEmpty(schedule.ProgramId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (images != null)
|
||||
{
|
||||
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
|
||||
@@ -149,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
var programEntry = programDict[schedule.ProgramId];
|
||||
|
||||
var allImages = images[imageIndex].Data ?? new List<ImageDataDto>();
|
||||
var allImages = images[imageIndex].Data;
|
||||
var imagesWithText = allImages.Where(i => string.Equals(i.Text, "yes", StringComparison.OrdinalIgnoreCase));
|
||||
var imagesWithoutText = allImages.Where(i => string.Equals(i.Text, "no", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -217,7 +231,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details)
|
||||
{
|
||||
var startAt = GetDate(programInfo.AirDateTime);
|
||||
if (programInfo.AirDateTime == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var startAt = programInfo.AirDateTime.Value;
|
||||
var endAt = startAt.AddSeconds(programInfo.Duration);
|
||||
var audioType = ProgramAudio.Stereo;
|
||||
|
||||
@@ -225,21 +244,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
string newID = programId + "T" + startAt.Ticks + "C" + channelId;
|
||||
|
||||
if (programInfo.AudioProperties != null)
|
||||
if (programInfo.AudioProperties.Count != 0)
|
||||
{
|
||||
if (programInfo.AudioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
|
||||
if (programInfo.AudioProperties.Contains("atmos", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
audioType = ProgramAudio.Atmos;
|
||||
}
|
||||
else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
|
||||
else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
audioType = ProgramAudio.DolbyDigital;
|
||||
}
|
||||
else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
|
||||
else if (programInfo.AudioProperties.Contains("dd", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
audioType = ProgramAudio.DolbyDigital;
|
||||
}
|
||||
else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
|
||||
else if (programInfo.AudioProperties.Contains("stereo", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
audioType = ProgramAudio.Stereo;
|
||||
}
|
||||
@@ -355,9 +374,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(details.OriginalAirDate))
|
||||
if (details.OriginalAirDate != null)
|
||||
{
|
||||
info.OriginalAirDate = DateTime.Parse(details.OriginalAirDate, CultureInfo.InvariantCulture);
|
||||
info.OriginalAirDate = details.OriginalAirDate;
|
||||
info.ProductionYear = info.OriginalAirDate.Value.Year;
|
||||
}
|
||||
|
||||
@@ -384,18 +403,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return info;
|
||||
}
|
||||
|
||||
private static DateTime GetDate(string value)
|
||||
{
|
||||
var date = DateTime.ParseExact(value, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", CultureInfo.InvariantCulture);
|
||||
|
||||
if (date.Kind != DateTimeKind.Utc)
|
||||
{
|
||||
date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect)
|
||||
{
|
||||
var match = images
|
||||
@@ -449,14 +456,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<List<ShowImagesDto>> GetImageForPrograms(
|
||||
private async Task<IReadOnlyList<ShowImagesDto>> GetImageForPrograms(
|
||||
ListingsProviderInfo info,
|
||||
IReadOnlyList<string> programIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (programIds.Count == 0)
|
||||
{
|
||||
return new List<ShowImagesDto>();
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
}
|
||||
|
||||
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
|
||||
@@ -480,13 +487,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<List<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting image info from schedules direct");
|
||||
|
||||
return new List<ShowImagesDto>();
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +516,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var root = await JsonSerializer.DeserializeAsync<List<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (root != null)
|
||||
{
|
||||
@@ -520,7 +527,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
lineups.Add(new NameIdPair
|
||||
{
|
||||
Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name,
|
||||
Id = lineup.Uri[18..]
|
||||
Id = lineup.Uri?[18..]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -651,7 +658,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (string.Equals(root.Message, "OK", StringComparison.Ordinal))
|
||||
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
|
||||
return root.Token;
|
||||
@@ -708,12 +715,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
using var response = httpResponse.Content;
|
||||
var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return root.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase));
|
||||
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
// SchedulesDirect returns 400 if no lineups are configured.
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
|
||||
if (ex.StatusCode is HttpStatusCode.BadRequest)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -779,10 +786,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (root == null)
|
||||
{
|
||||
return new List<ChannelInfo>();
|
||||
}
|
||||
|
||||
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.Map.Count);
|
||||
_logger.LogInformation("Mapping Stations to Channel");
|
||||
|
||||
var allStations = root.Stations ?? new List<StationDto>();
|
||||
var allStations = root.Stations;
|
||||
|
||||
var map = root.Map;
|
||||
var list = new List<ChannelInfo>(map.Count);
|
||||
@@ -790,11 +802,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
var channelNumber = GetChannelNumber(channel);
|
||||
|
||||
var station = allStations.Find(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase))
|
||||
?? new StationDto
|
||||
{
|
||||
StationId = channel.StationId
|
||||
};
|
||||
var stationIndex = allStations.FindIndex(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase));
|
||||
var station = stationIndex == -1
|
||||
? new StationDto { StationId = channel.StationId }
|
||||
: allStations[stationIndex];
|
||||
|
||||
var channelInfo = new ChannelInfo
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,24 +11,24 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the city.
|
||||
/// </summary>
|
||||
[JsonPropertyName("city")]
|
||||
public string City { get; set; }
|
||||
public string? City { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public string State { get; set; }
|
||||
public string? State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the postal code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("postalCode")]
|
||||
public string Postalcode { get; set; }
|
||||
public string? Postalcode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the country.
|
||||
/// </summary>
|
||||
[JsonPropertyName("country")]
|
||||
public string Country { get; set; }
|
||||
public string? Country { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the content.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; }
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lang.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lang")]
|
||||
public string Lang { get; set; }
|
||||
public string? Lang { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,36 +11,36 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the billing order.
|
||||
/// </summary>
|
||||
[JsonPropertyName("billingOrder")]
|
||||
public string BillingOrder { get; set; }
|
||||
public string? BillingOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; }
|
||||
public string? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("nameId")]
|
||||
public string NameId { get; set; }
|
||||
public string? NameId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the person id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("personId")]
|
||||
public string PersonId { get; set; }
|
||||
public string? PersonId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the character name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("characterName")]
|
||||
public string CharacterName { get; set; }
|
||||
public string? CharacterName { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -14,18 +13,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the list of maps.
|
||||
/// </summary>
|
||||
[JsonPropertyName("map")]
|
||||
public List<MapDto> Map { get; set; }
|
||||
public IReadOnlyList<MapDto> Map { get; set; } = Array.Empty<MapDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of stations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stations")]
|
||||
public List<StationDto> Stations { get; set; }
|
||||
public IReadOnlyList<StationDto> Stations { get; set; } = Array.Empty<StationDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public MetadataDto Metadata { get; set; }
|
||||
public MetadataDto? Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the body.
|
||||
/// </summary>
|
||||
[JsonPropertyName("body")]
|
||||
public string Body { get; set; }
|
||||
public string? Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public string Code { get; set; }
|
||||
public string? Code { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,30 +11,30 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the billing order.
|
||||
/// </summary>
|
||||
[JsonPropertyName("billingOrder")]
|
||||
public string BillingOrder { get; set; }
|
||||
public string? BillingOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; }
|
||||
public string? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("nameId")]
|
||||
public string NameId { get; set; }
|
||||
public string? NameId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the person id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("personId")]
|
||||
public string PersonId { get; set; }
|
||||
public string? PersonId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -10,30 +9,22 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// </summary>
|
||||
public class DayDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DayDto"/> class.
|
||||
/// </summary>
|
||||
public DayDto()
|
||||
{
|
||||
Programs = new List<ProgramDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string StationId { get; set; }
|
||||
public string? StationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of programs.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programs")]
|
||||
public List<ProgramDto> Programs { get; set; }
|
||||
public IReadOnlyList<ProgramDto> Programs { get; set; } = Array.Empty<ProgramDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata schedule.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public MetadataScheduleDto Metadata { get; set; }
|
||||
public MetadataScheduleDto? Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the description language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptionLanguage")]
|
||||
public string DescriptionLanguage { get; set; }
|
||||
public string? DescriptionLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the description language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptionLanguage")]
|
||||
public string DescriptionLanguage { get; set; }
|
||||
public string? DescriptionLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -14,12 +13,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the list of description 100.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description100")]
|
||||
public List<Description100Dto> Description100 { get; set; }
|
||||
public IReadOnlyList<Description100Dto> Description100 { get; set; } = Array.Empty<Description100Dto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of description1000.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description1000")]
|
||||
public List<Description1000Dto> Description1000 { get; set; }
|
||||
public IReadOnlyList<Description1000Dto> Description1000 { get; set; } = Array.Empty<Description1000Dto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,6 +11,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the sub type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("subType")]
|
||||
public string SubType { get; set; }
|
||||
public string? SubType { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -14,24 +13,24 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the headend.
|
||||
/// </summary>
|
||||
[JsonPropertyName("headend")]
|
||||
public string Headend { get; set; }
|
||||
public string? Headend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport")]
|
||||
public string Transport { get; set; }
|
||||
public string? Transport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("location")]
|
||||
public string Location { get; set; }
|
||||
public string? Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of lineups.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineups")]
|
||||
public List<LineupDto> Lineups { get; set; }
|
||||
public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,60 +11,60 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
[JsonPropertyName("width")]
|
||||
public string Width { get; set; }
|
||||
public string? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
[JsonPropertyName("height")]
|
||||
public string Height { get; set; }
|
||||
public string? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uri.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uri")]
|
||||
public string Uri { get; set; }
|
||||
public string? Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
[JsonPropertyName("size")]
|
||||
public string Size { get; set; }
|
||||
public string? Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the aspect.
|
||||
/// </summary>
|
||||
[JsonPropertyName("aspect")]
|
||||
public string aspect { get; set; }
|
||||
public string? Aspect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the category.
|
||||
/// </summary>
|
||||
[JsonPropertyName("category")]
|
||||
public string Category { get; set; }
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; }
|
||||
public string? Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary.
|
||||
/// </summary>
|
||||
[JsonPropertyName("primary")]
|
||||
public string Primary { get; set; }
|
||||
public string? Primary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tier")]
|
||||
public string Tier { get; set; }
|
||||
public string? Tier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the caption.
|
||||
/// </summary>
|
||||
[JsonPropertyName("caption")]
|
||||
public CaptionDto Caption { get; set; }
|
||||
public CaptionDto? Caption { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,30 +11,36 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the linup.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineup")]
|
||||
public string Lineup { get; set; }
|
||||
public string? Lineup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lineup name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport")]
|
||||
public string Transport { get; set; }
|
||||
public string? Transport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("location")]
|
||||
public string Location { get; set; }
|
||||
public string? Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uri.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uri")]
|
||||
public string Uri { get; set; }
|
||||
public string? Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this lineup was deleted.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isDeleted")]
|
||||
public bool? IsDeleted { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -20,18 +19,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the server id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("serverID")]
|
||||
public string ServerId { get; set; }
|
||||
public string? ServerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the datetime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("datetime")]
|
||||
public string Datetime { get; set; }
|
||||
public DateTime? LineupTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of lineups.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineups")]
|
||||
public List<LineupDto> Lineups { get; set; }
|
||||
public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,7 +11,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the url.
|
||||
/// </summary>
|
||||
[JsonPropertyName("URL")]
|
||||
public string Url { get; set; }
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
@@ -31,6 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the md5.
|
||||
/// </summary>
|
||||
[JsonPropertyName("md5")]
|
||||
public string Md5 { get; set; }
|
||||
public string? Md5 { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,19 +11,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string StationId { get; set; }
|
||||
public string? StationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channel.
|
||||
/// </summary>
|
||||
[JsonPropertyName("channel")]
|
||||
public string Channel { get; set; }
|
||||
public string? Channel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the provider callsign.
|
||||
/// </summary>
|
||||
[JsonPropertyName("providerCallsign")]
|
||||
public string? ProvderCallsign { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logical channel number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logicalChannelNumber")]
|
||||
public string LogicalChannelNumber { get; set; }
|
||||
public string? LogicalChannelNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uhfvhf.
|
||||
@@ -44,5 +48,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// </summary>
|
||||
[JsonPropertyName("atscMinor")]
|
||||
public int AtscMinor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the match type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("matchType")]
|
||||
public string? MatchType { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,18 +11,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the linup.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineup")]
|
||||
public string Lineup { get; set; }
|
||||
public string? Lineup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the modified timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("modified")]
|
||||
public string Modified { get; set; }
|
||||
public string? Modified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport")]
|
||||
public string Transport { get; set; }
|
||||
public string? Transport { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -12,7 +10,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// <summary>
|
||||
/// Gets or sets the gracenote object.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gracenote")]
|
||||
public GracenoteDto Gracenote { get; set; }
|
||||
[JsonPropertyName("Gracenote")]
|
||||
public GracenoteDto? Gracenote { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,25 +12,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the modified timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("modified")]
|
||||
public string Modified { get; set; }
|
||||
public string? Modified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the md5.
|
||||
/// </summary>
|
||||
[JsonPropertyName("md5")]
|
||||
public string Md5 { get; set; }
|
||||
public string? Md5 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("startDate")]
|
||||
public string StartDate { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the end date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("endDate")]
|
||||
public string EndDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the days count.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -14,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the year.
|
||||
/// </summary>
|
||||
[JsonPropertyName("year")]
|
||||
public string Year { get; set; }
|
||||
public string? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
@@ -26,6 +25,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the list of quality rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("qualityRating")]
|
||||
public List<QualityRatingDto> QualityRating { get; set; }
|
||||
public IReadOnlyList<QualityRatingDto> QualityRating { get; set; } = Array.Empty<QualityRatingDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -14,85 +13,85 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the audience.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audience")]
|
||||
public string Audience { get; set; }
|
||||
public string? Audience { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the program id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programID")]
|
||||
public string ProgramId { get; set; }
|
||||
public string? ProgramId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of titles.
|
||||
/// </summary>
|
||||
[JsonPropertyName("titles")]
|
||||
public List<TitleDto> Titles { get; set; }
|
||||
public IReadOnlyList<TitleDto> Titles { get; set; } = Array.Empty<TitleDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the event details object.
|
||||
/// </summary>
|
||||
[JsonPropertyName("eventDetails")]
|
||||
public EventDetailsDto EventDetails { get; set; }
|
||||
public EventDetailsDto? EventDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the descriptions.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptions")]
|
||||
public DescriptionsProgramDto Descriptions { get; set; }
|
||||
public DescriptionsProgramDto? Descriptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the original air date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("originalAirDate")]
|
||||
public string OriginalAirDate { get; set; }
|
||||
public DateTime? OriginalAirDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of genres.
|
||||
/// </summary>
|
||||
[JsonPropertyName("genres")]
|
||||
public List<string> Genres { get; set; }
|
||||
public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("episodeTitle150")]
|
||||
public string EpisodeTitle150 { get; set; }
|
||||
public string? EpisodeTitle150 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public List<MetadataProgramsDto> Metadata { get; set; }
|
||||
public IReadOnlyList<MetadataProgramsDto> Metadata { get; set; } = Array.Empty<MetadataProgramsDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of content raitings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("contentRating")]
|
||||
public List<ContentRatingDto> ContentRating { get; set; }
|
||||
public IReadOnlyList<ContentRatingDto> ContentRating { get; set; } = Array.Empty<ContentRatingDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of cast.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cast")]
|
||||
public List<CastDto> Cast { get; set; }
|
||||
public IReadOnlyList<CastDto> Cast { get; set; } = Array.Empty<CastDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of crew.
|
||||
/// </summary>
|
||||
[JsonPropertyName("crew")]
|
||||
public List<CrewDto> Crew { get; set; }
|
||||
public IReadOnlyList<CrewDto> Crew { get; set; } = Array.Empty<CrewDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entity type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("entityType")]
|
||||
public string EntityType { get; set; }
|
||||
public string? EntityType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the show type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("showType")]
|
||||
public string ShowType { get; set; }
|
||||
public string? ShowType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether there is image artwork.
|
||||
@@ -104,54 +103,54 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the primary image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("primaryImage")]
|
||||
public string PrimaryImage { get; set; }
|
||||
public string? PrimaryImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the thumb image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("thumbImage")]
|
||||
public string ThumbImage { get; set; }
|
||||
public string? ThumbImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backdrop image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("backdropImage")]
|
||||
public string BackdropImage { get; set; }
|
||||
public string? BackdropImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the banner image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("bannerImage")]
|
||||
public string BannerImage { get; set; }
|
||||
public string? BannerImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("imageID")]
|
||||
public string ImageId { get; set; }
|
||||
public string? ImageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the md5.
|
||||
/// </summary>
|
||||
[JsonPropertyName("md5")]
|
||||
public string Md5 { get; set; }
|
||||
public string? Md5 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of content advisory.
|
||||
/// </summary>
|
||||
[JsonPropertyName("contentAdvisory")]
|
||||
public List<string> ContentAdvisory { get; set; }
|
||||
public IReadOnlyList<string> ContentAdvisory { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the movie object.
|
||||
/// </summary>
|
||||
[JsonPropertyName("movie")]
|
||||
public MovieDto Movie { get; set; }
|
||||
public MovieDto? Movie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of recommendations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("recommendations")]
|
||||
public List<RecommendationDto> Recommendations { get; set; }
|
||||
public IReadOnlyList<RecommendationDto> Recommendations { get; set; } = Array.Empty<RecommendationDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -14,13 +13,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the program id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programID")]
|
||||
public string ProgramId { get; set; }
|
||||
public string? ProgramId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the air date time.
|
||||
/// </summary>
|
||||
[JsonPropertyName("airDateTime")]
|
||||
public string AirDateTime { get; set; }
|
||||
public DateTime? AirDateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
@@ -32,25 +31,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the md5.
|
||||
/// </summary>
|
||||
[JsonPropertyName("md5")]
|
||||
public string Md5 { get; set; }
|
||||
public string? Md5 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of audio properties.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audioProperties")]
|
||||
public List<string> AudioProperties { get; set; }
|
||||
public IReadOnlyList<string> AudioProperties { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of video properties.
|
||||
/// </summary>
|
||||
[JsonPropertyName("videoProperties")]
|
||||
public List<string> VideoProperties { get; set; }
|
||||
public IReadOnlyList<string> VideoProperties { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of ratings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ratings")]
|
||||
public List<RatingDto> Ratings { get; set; }
|
||||
public IReadOnlyList<RatingDto> Ratings { get; set; } = Array.Empty<RatingDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this program is new.
|
||||
@@ -62,13 +61,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the multipart object.
|
||||
/// </summary>
|
||||
[JsonPropertyName("multipart")]
|
||||
public MultipartDto Multipart { get; set; }
|
||||
public MultipartDto? Multipart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the live tape delay.
|
||||
/// </summary>
|
||||
[JsonPropertyName("liveTapeDelay")]
|
||||
public string LiveTapeDelay { get; set; }
|
||||
public string? LiveTapeDelay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this is the premiere.
|
||||
@@ -86,6 +85,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the premiere or finale.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isPremiereOrFinale")]
|
||||
public string IsPremiereOrFinale { get; set; }
|
||||
public string? IsPremiereOrFinale { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,30 +11,30 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the ratings body.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ratingsBody")]
|
||||
public string RatingsBody { get; set; }
|
||||
public string? RatingsBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rating")]
|
||||
public string Rating { get; set; }
|
||||
public string? Rating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the min rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("minRating")]
|
||||
public string MinRating { get; set; }
|
||||
public string? MinRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the max rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("maxRating")]
|
||||
public string MaxRating { get; set; }
|
||||
public string? MaxRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the increment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("increment")]
|
||||
public string Increment { get; set; }
|
||||
public string? Increment { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the body.
|
||||
/// </summary>
|
||||
[JsonPropertyName("body")]
|
||||
public string Body { get; set; }
|
||||
public string? Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public string Code { get; set; }
|
||||
public string? Code { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the program id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programID")]
|
||||
public string ProgramId { get; set; }
|
||||
public string? ProgramId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title120")]
|
||||
public string Title120 { get; set; }
|
||||
public string? Title120 { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -14,12 +13,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string StationId { get; set; }
|
||||
public string? StationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of dates.
|
||||
/// </summary>
|
||||
[JsonPropertyName("date")]
|
||||
public List<string> Date { get; set; }
|
||||
public IReadOnlyList<string> Date { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -14,12 +13,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the program id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programID")]
|
||||
public string ProgramId { get; set; }
|
||||
public string? ProgramId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of data.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public List<ImageDataDto> Data { get; set; }
|
||||
public IReadOnlyList<ImageDataDto> Data { get; set; } = Array.Empty<ImageDataDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,66 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Station dto.
|
||||
/// </summary>
|
||||
public class StationDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string StationId { get; set; }
|
||||
/// Station dto.
|
||||
/// </summary>
|
||||
public class StationDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string? StationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callsign.
|
||||
/// </summary>
|
||||
[JsonPropertyName("callsign")]
|
||||
public string Callsign { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the callsign.
|
||||
/// </summary>
|
||||
[JsonPropertyName("callsign")]
|
||||
public string? Callsign { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the broadcast language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("broadcastLanguage")]
|
||||
public List<string> BroadcastLanguage { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the broadcast language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("broadcastLanguage")]
|
||||
public IReadOnlyList<string> BroadcastLanguage { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptionLanguage")]
|
||||
public List<string> DescriptionLanguage { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the description language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptionLanguage")]
|
||||
public IReadOnlyList<string> DescriptionLanguage { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the broadcaster.
|
||||
/// </summary>
|
||||
[JsonPropertyName("broadcaster")]
|
||||
public BroadcasterDto Broadcaster { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the broadcaster.
|
||||
/// </summary>
|
||||
[JsonPropertyName("broadcaster")]
|
||||
public BroadcasterDto? Broadcaster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the affiliate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("affiliate")]
|
||||
public string Affiliate { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the affiliate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("affiliate")]
|
||||
public string? Affiliate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logo.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logo")]
|
||||
public LogoDto Logo { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the logo.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logo")]
|
||||
public LogoDto? Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set a value indicating whether it is commercial free.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isCommercialFree")]
|
||||
public bool? IsCommercialFree { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether it is commercial free.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isCommercialFree")]
|
||||
public bool? IsCommercialFree { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -13,6 +11,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title120")]
|
||||
public string Title120 { get; set; }
|
||||
public string? Title120 { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
@@ -19,18 +18,30 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
/// Gets or sets the response message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; }
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("serverID")]
|
||||
public string ServerId { get; set; }
|
||||
public string? ServerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the token.
|
||||
/// </summary>
|
||||
[JsonPropertyName("token")]
|
||||
public string Token { get; set; }
|
||||
public string? Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current datetime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("datetime")]
|
||||
public DateTime? TokenTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("response")]
|
||||
public string? Response { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.XmlTv;
|
||||
using Jellyfin.XmlTv.Entities;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
@@ -59,41 +60,41 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return _config.Configuration.PreferredMetadataLanguage;
|
||||
}
|
||||
|
||||
private async Task<string> GetXml(string path, CancellationToken cancellationToken)
|
||||
private async Task<string> GetXml(ListingsProviderInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("xmltv path: {Path}", path);
|
||||
_logger.LogInformation("xmltv path: {Path}", info.Path);
|
||||
|
||||
if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return UnzipIfNeeded(path, path);
|
||||
return UnzipIfNeeded(info.Path, info.Path);
|
||||
}
|
||||
|
||||
string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml";
|
||||
string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + "-" + info.Id + ".xml";
|
||||
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
||||
if (File.Exists(cacheFile))
|
||||
{
|
||||
return UnzipIfNeeded(path, cacheFile);
|
||||
return UnzipIfNeeded(info.Path, cacheFile);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Downloading xmltv listings from {Path}", path);
|
||||
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
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))
|
||||
await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous))
|
||||
{
|
||||
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return UnzipIfNeeded(path, cacheFile);
|
||||
return UnzipIfNeeded(info.Path, cacheFile);
|
||||
}
|
||||
|
||||
private string UnzipIfNeeded(string originalUrl, string file)
|
||||
private string UnzipIfNeeded(ReadOnlySpan<char> originalUrl, string file)
|
||||
{
|
||||
string ext = Path.GetExtension(originalUrl.Split('?')[0]);
|
||||
ReadOnlySpan<char> ext = Path.GetExtension(originalUrl.LeftPart('?'));
|
||||
|
||||
if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -162,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
_logger.LogDebug("Getting xmltv programs for channel {Id}", channelId);
|
||||
|
||||
string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
|
||||
string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogDebug("Opening XmlTvReader for {Path}", path);
|
||||
var reader = new XmlTvReader(path, GetLanguage(info));
|
||||
|
||||
@@ -256,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
public async Task<List<NameIdPair>> 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);
|
||||
string path = await GetXml(info, CancellationToken.None).ConfigureAwait(false);
|
||||
_logger.LogDebug("Opening XmlTvReader for {Path}", path);
|
||||
var reader = new XmlTvReader(path, GetLanguage(info));
|
||||
IEnumerable<XmlTvChannel> results = reader.GetChannels();
|
||||
@@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
public async Task<List<ChannelInfo>> 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);
|
||||
string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogDebug("Opening XmlTvReader for {Path}", path);
|
||||
var reader = new XmlTvReader(path, GetLanguage(info));
|
||||
var results = reader.GetChannels();
|
||||
|
||||
@@ -23,10 +23,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public abstract class BaseTunerHost
|
||||
{
|
||||
protected readonly IServerConfigurationManager Config;
|
||||
protected readonly ILogger<BaseTunerHost> Logger;
|
||||
protected readonly IFileSystem FileSystem;
|
||||
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
|
||||
@@ -37,12 +33,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
FileSystem = fileSystem;
|
||||
}
|
||||
|
||||
protected IServerConfigurationManager Config { get; }
|
||||
|
||||
protected ILogger<BaseTunerHost> Logger { get; }
|
||||
|
||||
protected IFileSystem FileSystem { get; }
|
||||
|
||||
public virtual bool IsSupported => true;
|
||||
|
||||
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
|
||||
|
||||
public abstract string Type { get; }
|
||||
|
||||
protected virtual string ChannelIdPrefix => Type + "_";
|
||||
|
||||
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
|
||||
|
||||
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = tuner.Id;
|
||||
@@ -92,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
|
||||
await using var writeStream = File.OpenWrite(channelCacheFile);
|
||||
await using var writeStream = AsyncFile.OpenWrite(channelCacheFile);
|
||||
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (IOException)
|
||||
@@ -108,7 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var readStream = File.OpenRead(channelCacheFile);
|
||||
await using var readStream = AsyncFile.OpenRead(channelCacheFile);
|
||||
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
list.AddRange(channels);
|
||||
@@ -217,8 +221,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
throw new LiveTvConflictException();
|
||||
}
|
||||
|
||||
protected virtual string ChannelIdPrefix => Type + "_";
|
||||
|
||||
protected virtual bool IsValidChannelId(string channelId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(channelId))
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class HdHomerunChannelCommands : IHdHomerunChannelCommands
|
||||
{
|
||||
private string? _channel;
|
||||
private string? _profile;
|
||||
|
||||
public HdHomerunChannelCommands(string? channel, string? profile)
|
||||
{
|
||||
_channel = channel;
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
public IEnumerable<(string, string)> GetCommands()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_channel))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_profile)
|
||||
&& !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
yield return ("vchannel", $"{_channel} transcode={_profile}");
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return ("vchannel", _channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
@@ -50,7 +49,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IServerApplicationHost appHost,
|
||||
ISocketFactory socketFactory,
|
||||
INetworkManager networkManager,
|
||||
IStreamHelper streamHelper,
|
||||
IMemoryCache memoryCache)
|
||||
: base(config, logger, fileSystem, memoryCache)
|
||||
@@ -58,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_appHost = appHost;
|
||||
_socketFactory = socketFactory;
|
||||
_networkManager = networkManager;
|
||||
_streamHelper = streamHelper;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
@@ -70,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
protected override string ChannelIdPrefix => "hdhr_";
|
||||
|
||||
private string GetChannelId(TunerHostInfo info, Channels i)
|
||||
private string GetChannelId(Channels i)
|
||||
=> ChannelIdPrefix + i.GuideNumber;
|
||||
|
||||
internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
@@ -90,11 +87,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
return lineup.Where(i => !i.DRM).ToList();
|
||||
}
|
||||
|
||||
private class HdHomerunChannelInfo : ChannelInfo
|
||||
{
|
||||
public bool IsLegacyTuner { get; set; }
|
||||
}
|
||||
|
||||
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
|
||||
{
|
||||
var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false);
|
||||
@@ -103,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
Name = i.GuideName,
|
||||
Number = i.GuideNumber,
|
||||
Id = GetChannelId(tuner, i),
|
||||
Id = GetChannelId(i),
|
||||
IsFavorite = i.Favorite,
|
||||
TunerHostId = tuner.Id,
|
||||
IsHD = i.HD,
|
||||
@@ -255,7 +247,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var tuners = new List<LiveTvTunerInfo>();
|
||||
var tuners = new List<LiveTvTunerInfo>(model.TunerCount);
|
||||
|
||||
var uri = new Uri(GetApiUrl(info));
|
||||
|
||||
@@ -264,10 +256,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
// Legacy HdHomeruns are IPv4 only
|
||||
var ipInfo = IPAddress.Parse(uri.Host);
|
||||
|
||||
for (int i = 0; i < model.TunerCount; ++i)
|
||||
for (int i = 0; i < model.TunerCount; i++)
|
||||
{
|
||||
var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
|
||||
var currentChannel = "none"; // @todo Get current channel and map back to Station Id
|
||||
var currentChannel = "none"; // TODO: Get current channel and map back to Station Id
|
||||
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
|
||||
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
|
||||
tuners.Add(new LiveTvTunerInfo
|
||||
@@ -455,28 +447,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
Path = url,
|
||||
Protocol = MediaProtocol.Udp,
|
||||
MediaStreams = new List<MediaStream>
|
||||
{
|
||||
new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Video,
|
||||
// Set the index to -1 because we don't know the exact index of the video stream within the container
|
||||
Index = -1,
|
||||
IsInterlaced = isInterlaced,
|
||||
Codec = videoCodec,
|
||||
Width = width,
|
||||
Height = height,
|
||||
BitRate = videoBitrate,
|
||||
NalLengthSize = nal
|
||||
},
|
||||
new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Audio,
|
||||
// Set the index to -1 because we don't know the exact index of the audio stream within the container
|
||||
Index = -1,
|
||||
Codec = audioCodec,
|
||||
BitRate = audioBitrate
|
||||
}
|
||||
},
|
||||
{
|
||||
new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Video,
|
||||
// Set the index to -1 because we don't know the exact index of the video stream within the container
|
||||
Index = -1,
|
||||
IsInterlaced = isInterlaced,
|
||||
Codec = videoCodec,
|
||||
Width = width,
|
||||
Height = height,
|
||||
BitRate = videoBitrate,
|
||||
NalLengthSize = nal
|
||||
},
|
||||
new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Audio,
|
||||
// Set the index to -1 because we don't know the exact index of the audio stream within the container
|
||||
Index = -1,
|
||||
Codec = audioCodec,
|
||||
BitRate = audioBitrate
|
||||
}
|
||||
},
|
||||
RequiresOpening = true,
|
||||
RequiresClosing = true,
|
||||
BufferMs = 0,
|
||||
@@ -551,7 +543,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
var profile = streamId.Split('_')[0];
|
||||
var profile = streamId.AsSpan().LeftPart('_').ToString();
|
||||
|
||||
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile);
|
||||
|
||||
@@ -718,5 +710,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
return hostInfo;
|
||||
}
|
||||
|
||||
private class HdHomerunChannelInfo : ChannelInfo
|
||||
{
|
||||
public bool IsLegacyTuner { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common;
|
||||
@@ -18,70 +16,6 @@ using MediaBrowser.Controller.LiveTv;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public interface IHdHomerunChannelCommands
|
||||
{
|
||||
IEnumerable<(string, string)> GetCommands();
|
||||
}
|
||||
|
||||
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
|
||||
{
|
||||
private string _channel;
|
||||
private string _program;
|
||||
|
||||
public LegacyHdHomerunChannelCommands(string url)
|
||||
{
|
||||
// parse url for channel and program
|
||||
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
|
||||
var match = regExp.Match(url);
|
||||
if (match.Success)
|
||||
{
|
||||
_channel = match.Groups[1].Value;
|
||||
_program = match.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(string, string)> GetCommands()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_channel))
|
||||
{
|
||||
yield return ("channel", _channel);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_program))
|
||||
{
|
||||
yield return ("program", _program);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class HdHomerunChannelCommands : IHdHomerunChannelCommands
|
||||
{
|
||||
private string _channel;
|
||||
private string _profile;
|
||||
|
||||
public HdHomerunChannelCommands(string channel, string profile)
|
||||
{
|
||||
_channel = channel;
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
public IEnumerable<(string, string)> GetCommands()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_channel))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_profile)
|
||||
&& !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
yield return ("vchannel", $"{_channel} transcode={_profile}");
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return ("vchannel", _channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HdHomerunManager : IDisposable
|
||||
{
|
||||
public const int HdHomeRunPort = 65001;
|
||||
@@ -150,8 +84,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
if (!_lockkey.HasValue)
|
||||
{
|
||||
var rand = new Random();
|
||||
_lockkey = (uint)rand.Next();
|
||||
_lockkey = (uint)Random.Shared.Next();
|
||||
}
|
||||
|
||||
var lockKeyValue = _lockkey.Value;
|
||||
|
||||
@@ -101,7 +101,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
if (localAddress.IsIPv4MappedToIPv6) {
|
||||
if (localAddress.IsIPv4MappedToIPv6)
|
||||
{
|
||||
localAddress = localAddress.MapToIPv4();
|
||||
}
|
||||
|
||||
@@ -156,11 +157,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetFilePath()
|
||||
{
|
||||
return TempFilePath;
|
||||
}
|
||||
|
||||
private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
using (udpClient)
|
||||
@@ -184,7 +180,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
EnableStreamSharing = false;
|
||||
}
|
||||
|
||||
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
|
||||
await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
@@ -201,7 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
cancellationToken,
|
||||
timeOutSource.Token))
|
||||
{
|
||||
var resTask = udpClient.ReceiveAsync();
|
||||
var resTask = udpClient.ReceiveAsync(linkedSource.Token).AsTask();
|
||||
if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask)
|
||||
{
|
||||
resTask.Dispose();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public interface IHdHomerunChannelCommands
|
||||
{
|
||||
IEnumerable<(string, string)> GetCommands();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
|
||||
{
|
||||
private string? _channel;
|
||||
private string? _program;
|
||||
|
||||
public LegacyHdHomerunChannelCommands(string url)
|
||||
{
|
||||
// parse url for channel and program
|
||||
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
|
||||
var match = regExp.Match(url);
|
||||
if (match.Success)
|
||||
{
|
||||
_channel = match.Groups[1].Value;
|
||||
_program = match.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(string, string)> GetCommands()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_channel))
|
||||
{
|
||||
yield return ("channel", _channel);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_program))
|
||||
{
|
||||
yield return ("program", _program);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,8 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
@@ -22,14 +20,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
protected readonly IFileSystem FileSystem;
|
||||
|
||||
protected readonly IStreamHelper StreamHelper;
|
||||
|
||||
protected string TempFilePath;
|
||||
protected readonly ILogger Logger;
|
||||
protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public LiveStream(
|
||||
MediaSourceInfo mediaSource,
|
||||
TunerHostInfo tuner,
|
||||
@@ -57,7 +47,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
SetTempFilePath("ts");
|
||||
}
|
||||
|
||||
protected virtual int EmptyReadLimit => 1000;
|
||||
protected IFileSystem FileSystem { get; }
|
||||
|
||||
protected IStreamHelper StreamHelper { get; }
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
protected CancellationTokenSource LiveStreamCancellationTokenSource { get; } = new CancellationTokenSource();
|
||||
|
||||
protected string TempFilePath { get; set; }
|
||||
|
||||
public MediaSourceInfo OriginalMediaSource { get; set; }
|
||||
|
||||
@@ -97,123 +95,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected FileStream GetInputStream(string path, bool allowAsyncFileRead)
|
||||
public Stream GetStream()
|
||||
{
|
||||
var stream = GetInputStream(TempFilePath);
|
||||
bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
|
||||
if (seekFile)
|
||||
{
|
||||
TrySeek(stream, -20000);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
protected FileStream GetInputStream(string path)
|
||||
=> new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
IODefaults.FileStreamBufferSize,
|
||||
allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
|
||||
FileOptions.SequentialScan | FileOptions.Asynchronous);
|
||||
|
||||
public Task DeleteTempFiles()
|
||||
{
|
||||
return DeleteTempFiles(GetStreamFilePaths());
|
||||
}
|
||||
|
||||
protected async Task DeleteTempFiles(IEnumerable<string> paths, int retryCount = 0)
|
||||
protected async Task DeleteTempFiles(string path, int retryCount = 0)
|
||||
{
|
||||
if (retryCount == 0)
|
||||
{
|
||||
Logger.LogInformation("Deleting temp files {0}", paths);
|
||||
Logger.LogInformation("Deleting temp file {FilePath}", path);
|
||||
}
|
||||
|
||||
var failedFiles = new List<string>();
|
||||
|
||||
foreach (var path in paths)
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
FileSystem.DeleteFile(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error deleting file {FilePath}", path);
|
||||
if (retryCount <= 40)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
FileSystem.DeleteFile(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error deleting file {path}", path);
|
||||
failedFiles.Add(path);
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
await DeleteTempFiles(path, retryCount + 1).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (failedFiles.Count > 0 && retryCount <= 40)
|
||||
{
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
await DeleteTempFiles(failedFiles, retryCount + 1).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual List<string> GetStreamFilePaths()
|
||||
{
|
||||
return new List<string> { TempFilePath };
|
||||
}
|
||||
|
||||
public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
|
||||
cancellationToken = linkedCancellationTokenSource.Token;
|
||||
|
||||
// use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039
|
||||
var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT;
|
||||
|
||||
bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
|
||||
|
||||
var nextFileInfo = GetNextFile(null);
|
||||
var nextFile = nextFileInfo.file;
|
||||
var isLastFile = nextFileInfo.isLastFile;
|
||||
|
||||
while (!string.IsNullOrEmpty(nextFile))
|
||||
{
|
||||
var emptyReadLimit = isLastFile ? EmptyReadLimit : 1;
|
||||
|
||||
await CopyFile(nextFile, seekFile, emptyReadLimit, allowAsync, stream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
seekFile = false;
|
||||
nextFileInfo = GetNextFile(nextFile);
|
||||
nextFile = nextFileInfo.file;
|
||||
isLastFile = nextFileInfo.isLastFile;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Live Stream ended.");
|
||||
}
|
||||
|
||||
private (string file, bool isLastFile) GetNextFile(string currentFile)
|
||||
{
|
||||
var files = GetStreamFilePaths();
|
||||
|
||||
if (string.IsNullOrEmpty(currentFile))
|
||||
{
|
||||
return (files[^1], true);
|
||||
}
|
||||
|
||||
var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
|
||||
|
||||
var isLastFile = nextIndex == files.Count - 1;
|
||||
|
||||
return (files.ElementAtOrDefault(nextIndex), isLastFile);
|
||||
}
|
||||
|
||||
private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var inputStream = GetInputStream(path, allowAsync))
|
||||
{
|
||||
if (seekFile)
|
||||
{
|
||||
TrySeek(inputStream, -20000);
|
||||
}
|
||||
|
||||
await StreamHelper.CopyToAsync(
|
||||
inputStream,
|
||||
stream,
|
||||
IODefaults.CopyToBufferSize,
|
||||
emptyReadLimit,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void TrySeek(FileStream stream, long offset)
|
||||
private void TrySeek(Stream stream, long offset)
|
||||
{
|
||||
if (!stream.CanSeek)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -50,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return File.OpenRead(info.Url);
|
||||
return AsyncFile.OpenRead(info.Url);
|
||||
}
|
||||
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
|
||||
@@ -237,7 +238,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
try
|
||||
{
|
||||
numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
|
||||
numberString = Path.GetFileNameWithoutExtension(mediaUrl.AsSpan().RightPart('/')).ToString();
|
||||
|
||||
if (!IsValidChannelNumber(numberString))
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
@@ -55,39 +54,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
|
||||
|
||||
var typeName = GetType().Name;
|
||||
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
|
||||
Logger.LogInformation("Opening {StreamType} Live stream from {Url}", typeName, url);
|
||||
|
||||
// Response stream is disposed manually.
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var extension = "ts";
|
||||
var requiresRemux = false;
|
||||
|
||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
|
||||
if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
|
||||
|| contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
|
||||
|| contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
|
||||
|| contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
|
||||
|| contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
requiresRemux = true;
|
||||
}
|
||||
else if (contentType.IndexOf("mp4", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
contentType.IndexOf("dash", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
contentType.IndexOf("mpegURL", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
contentType.IndexOf("text/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
requiresRemux = true;
|
||||
// Close the stream without any sharing features
|
||||
response.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the stream without any sharing features
|
||||
if (requiresRemux)
|
||||
{
|
||||
using (response)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SetTempFilePath(extension);
|
||||
SetTempFilePath("ts");
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
@@ -117,49 +103,46 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
if (!taskCompletionSource.Task.Result)
|
||||
{
|
||||
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
|
||||
Logger.LogWarning("Zero bytes copied from stream {StreamType} to {FilePath} but no exception raised", GetType().Name, TempFilePath);
|
||||
throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
|
||||
}
|
||||
}
|
||||
|
||||
public string GetFilePath()
|
||||
{
|
||||
return TempFilePath;
|
||||
}
|
||||
|
||||
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
return Task.Run(
|
||||
async () =>
|
||||
{
|
||||
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
|
||||
using var message = response;
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
await StreamHelper.CopyToAsync(
|
||||
stream,
|
||||
fileStream,
|
||||
IODefaults.CopyToBufferSize,
|
||||
() => Resolve(openTaskCompletionSource),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
try
|
||||
{
|
||||
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
|
||||
using var message = response;
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await StreamHelper.CopyToAsync(
|
||||
stream,
|
||||
fileStream,
|
||||
IODefaults.CopyToBufferSize,
|
||||
() => Resolve(openTaskCompletionSource),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Logger.LogInformation("Copying of {StreamType} to {FilePath} was canceled", GetType().Name, TempFilePath);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error copying live stream {StreamType} to {FilePath}", GetType().Name, TempFilePath);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
|
||||
openTaskCompletionSource.TrySetResult(false);
|
||||
openTaskCompletionSource.TrySetResult(false);
|
||||
|
||||
EnableStreamSharing = false;
|
||||
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
|
||||
}, CancellationToken.None);
|
||||
EnableStreamSharing = false;
|
||||
await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)
|
||||
|
||||
Reference in New Issue
Block a user