mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-17 19:54:47 +01:00
Merge branch 'master' into comparisons
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -7,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;
|
||||
@@ -33,7 +32,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
public Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
if (directStreamProvider != null)
|
||||
{
|
||||
@@ -45,23 +44,29 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||
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,10 +76,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
_logger.LogInformation("Opened recording stream from tuner provider");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||
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();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using System.Xml;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
@@ -159,8 +160,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
try
|
||||
{
|
||||
var recordingFolders = GetRecordingFolders().ToArray();
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders()
|
||||
.ToList();
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders();
|
||||
|
||||
var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
|
||||
|
||||
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
continue;
|
||||
}
|
||||
|
||||
var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
|
||||
var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray();
|
||||
|
||||
var libraryOptions = new LibraryOptions
|
||||
{
|
||||
@@ -210,7 +210,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
foreach (var path in pathsToRemove)
|
||||
{
|
||||
await RemovePathFromLibrary(path).ConfigureAwait(false);
|
||||
await RemovePathFromLibraryAsync(path).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -219,17 +219,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemovePathFromLibrary(string path)
|
||||
private async Task RemovePathFromLibraryAsync(string path)
|
||||
{
|
||||
_logger.LogDebug("Removing path from library: {0}", path);
|
||||
|
||||
var requiresRefresh = false;
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders()
|
||||
.ToList();
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders();
|
||||
|
||||
foreach (var virtualFolder in virtualFolders)
|
||||
{
|
||||
if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
if (!virtualFolder.Locations.Contains(path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -460,7 +459,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
|
||||
{
|
||||
var tunerChannelId = tunerChannel.TunerChannelId;
|
||||
if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
|
||||
}
|
||||
@@ -612,16 +611,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
|
||||
public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ?
|
||||
var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
|
||||
null :
|
||||
_timerProvider.GetTimerByProgramId(timer.ProgramId);
|
||||
_timerProvider.GetTimerByProgramId(info.ProgramId);
|
||||
|
||||
if (existingTimer != null)
|
||||
{
|
||||
if (existingTimer.Status == RecordingStatus.Cancelled ||
|
||||
existingTimer.Status == RecordingStatus.Completed)
|
||||
if (existingTimer.Status == RecordingStatus.Cancelled
|
||||
|| existingTimer.Status == RecordingStatus.Completed)
|
||||
{
|
||||
existingTimer.Status = RecordingStatus.New;
|
||||
existingTimer.IsManual = true;
|
||||
@@ -634,32 +633,32 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
timer.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
LiveTvProgram programInfo = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(timer.ProgramId))
|
||||
if (!string.IsNullOrWhiteSpace(info.ProgramId))
|
||||
{
|
||||
programInfo = GetProgramInfoFromCache(timer);
|
||||
programInfo = GetProgramInfoFromCache(info);
|
||||
}
|
||||
|
||||
if (programInfo == null)
|
||||
{
|
||||
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
|
||||
programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
|
||||
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId);
|
||||
programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
|
||||
}
|
||||
|
||||
if (programInfo != null)
|
||||
{
|
||||
CopyProgramInfoToTimerInfo(programInfo, timer);
|
||||
CopyProgramInfoToTimerInfo(programInfo, info);
|
||||
}
|
||||
|
||||
timer.IsManual = true;
|
||||
_timerProvider.Add(timer);
|
||||
info.IsManual = true;
|
||||
_timerProvider.Add(info);
|
||||
|
||||
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
|
||||
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
|
||||
|
||||
return Task.FromResult(timer.Id);
|
||||
return Task.FromResult(info.Id);
|
||||
}
|
||||
|
||||
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
|
||||
@@ -893,7 +892,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
throw new ArgumentNullException(nameof(tunerHostId));
|
||||
}
|
||||
|
||||
return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
|
||||
return info.EnabledTuners.Contains(tunerHostId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
|
||||
@@ -913,18 +912,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
List<ProgramInfo> programs;
|
||||
|
||||
if (epgChannel == null)
|
||||
{
|
||||
_logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
|
||||
programs = new List<ProgramInfo>();
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
|
||||
|
||||
List<ProgramInfo> programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
|
||||
.ConfigureAwait(false)).ToList();
|
||||
}
|
||||
|
||||
// Replace the value that came from the provider with a normalized value
|
||||
foreach (var program in programs)
|
||||
@@ -940,7 +935,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
return new List<ProgramInfo>();
|
||||
return Enumerable.Empty<ProgramInfo>();
|
||||
}
|
||||
|
||||
private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
|
||||
@@ -963,7 +958,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Streaming Channel " + channelId);
|
||||
_logger.LogInformation("Streaming Channel {Id}", channelId);
|
||||
|
||||
var result = string.IsNullOrEmpty(streamId) ?
|
||||
null :
|
||||
@@ -1033,7 +1028,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
var stream = new MediaSourceInfo
|
||||
{
|
||||
EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
|
||||
EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
|
||||
EncoderProtocol = MediaProtocol.Http,
|
||||
Path = info.Path,
|
||||
Protocol = MediaProtocol.File,
|
||||
@@ -1292,7 +1287,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
_logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
_logger.LogInformation("Writing file to path: " + recordPath);
|
||||
_logger.LogInformation("Writing file to: {Path}", recordPath);
|
||||
|
||||
Action onStarted = async () =>
|
||||
{
|
||||
@@ -1314,16 +1309,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
recordingStatus = RecordingStatus.Completed;
|
||||
_logger.LogInformation("Recording completed: {recordPath}", recordPath);
|
||||
_logger.LogInformation("Recording completed: {RecordPath}", recordPath);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Recording stopped: {recordPath}", recordPath);
|
||||
_logger.LogInformation("Recording stopped: {RecordPath}", recordPath);
|
||||
recordingStatus = RecordingStatus.Completed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error recording to {recordPath}", recordPath);
|
||||
_logger.LogError(ex, "Error recording to {RecordPath}", recordPath);
|
||||
recordingStatus = RecordingStatus.Error;
|
||||
}
|
||||
|
||||
@@ -1410,20 +1405,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting 0-byte failed recording file {path}", path);
|
||||
_logger.LogError(ex, "Error deleting 0-byte failed recording file {Path}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerRefresh(string path)
|
||||
{
|
||||
_logger.LogInformation("Triggering refresh on {path}", path);
|
||||
_logger.LogInformation("Triggering refresh on {Path}", path);
|
||||
|
||||
var item = GetAffectedBaseItem(Path.GetDirectoryName(path));
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
_logger.LogInformation("Refreshing recording parent {path}", item.Path);
|
||||
_logger.LogInformation("Refreshing recording parent {Path}", item.Path);
|
||||
|
||||
_providerManager.QueueRefresh(
|
||||
item.Id,
|
||||
@@ -1458,7 +1453,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parentItem = item.GetParent();
|
||||
if (parentItem != null && !(parentItem is AggregateFolder))
|
||||
if (parentItem != null && parentItem is not AggregateFolder)
|
||||
{
|
||||
item = parentItem;
|
||||
}
|
||||
@@ -1512,8 +1507,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
DeleteLibraryItemsForTimers(timersToDelete);
|
||||
|
||||
var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
|
||||
if (librarySeries == null)
|
||||
if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1667,7 +1661,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
_logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Exited += Process_Exited;
|
||||
process.Exited += OnProcessExited;
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1681,7 +1675,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void Process_Exited(object sender, EventArgs e)
|
||||
private void OnProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
using (var process = (Process)sender)
|
||||
{
|
||||
@@ -1785,7 +1779,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
Limit = 1,
|
||||
ExternalId = timer.ProgramId,
|
||||
DtoOptions = new DtoOptions(true)
|
||||
@@ -1855,14 +1849,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))
|
||||
@@ -1920,14 +1912,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();
|
||||
@@ -1997,7 +1987,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)
|
||||
{
|
||||
@@ -2148,7 +2138,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
Limit = 1,
|
||||
DtoOptions = new DtoOptions(true)
|
||||
{
|
||||
@@ -2239,7 +2229,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
var enabledTimersForSeries = new List<TimerInfo>();
|
||||
foreach (var timer in allTimers)
|
||||
{
|
||||
var existingTimer = _timerProvider.GetTimer(timer.Id)
|
||||
var existingTimer = _timerProvider.GetTimer(timer.Id)
|
||||
?? (string.IsNullOrWhiteSpace(timer.ProgramId)
|
||||
? null
|
||||
: _timerProvider.GetTimerByProgramId(timer.ProgramId));
|
||||
@@ -2343,7 +2333,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
var deletes = _timerProvider.GetAll()
|
||||
.Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
|
||||
.Where(i => !allTimerIds.Contains(i.Id, StringComparison.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
|
||||
.Where(i => deleteStatuses.Contains(i.Status))
|
||||
.ToList();
|
||||
|
||||
@@ -2363,7 +2353,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
ExternalSeriesId = seriesTimer.SeriesId,
|
||||
DtoOptions = new DtoOptions(true)
|
||||
{
|
||||
@@ -2398,7 +2388,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
channel = _libraryManager.GetItemList(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
|
||||
ItemIds = new[] { parent.ChannelId },
|
||||
DtoOptions = new DtoOptions()
|
||||
}).FirstOrDefault() as LiveTvChannel;
|
||||
@@ -2457,7 +2447,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
channel = _libraryManager.GetItemList(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
|
||||
ItemIds = new[] { programInfo.ChannelId },
|
||||
DtoOptions = new DtoOptions()
|
||||
}).FirstOrDefault() as LiveTvChannel;
|
||||
@@ -2522,7 +2512,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
var seriesIds = _libraryManager.GetItemIds(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Series) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series },
|
||||
Name = program.Name
|
||||
}).ToArray();
|
||||
|
||||
@@ -2535,7 +2525,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
var result = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Episode) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
ParentIndexNumber = program.SeasonNumber.Value,
|
||||
IndexNumber = program.EpisodeNumber.Value,
|
||||
AncestorIds = seriesIds,
|
||||
@@ -2632,7 +2622,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
if (newDevicesOnly)
|
||||
{
|
||||
discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
|
||||
discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -87,17 +87,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
ErrorDialog = false
|
||||
};
|
||||
|
||||
var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments;
|
||||
_logger.LogInformation(commandLineLogMessage);
|
||||
_logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments);
|
||||
|
||||
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
|
||||
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);
|
||||
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processStartInfo.FileName + " " + processStartInfo.Arguments + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_process = new Process
|
||||
{
|
||||
@@ -188,7 +187,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("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
|
||||
videoArgs,
|
||||
GetAudioArgs(mediaSource),
|
||||
subtitleArgs,
|
||||
@@ -205,9 +204,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";
|
||||
}
|
||||
|
||||
@@ -225,13 +224,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Stopping ffmpeg recording process for {path}", _targetPath);
|
||||
_logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath);
|
||||
|
||||
_process.StandardInput.WriteLine("q");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error stopping recording transcoding job for {path}", _targetPath);
|
||||
_logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath);
|
||||
}
|
||||
|
||||
if (_hasExited)
|
||||
@@ -241,7 +240,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Calling recording process.WaitForExit for {path}", _targetPath);
|
||||
_logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath);
|
||||
|
||||
if (_process.WaitForExit(10000))
|
||||
{
|
||||
@@ -250,7 +249,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error waiting for recording process to exit for {path}", _targetPath);
|
||||
_logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath);
|
||||
}
|
||||
|
||||
if (_hasExited)
|
||||
@@ -260,13 +259,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Killing ffmpeg recording process for {path}", _targetPath);
|
||||
_logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath);
|
||||
|
||||
_process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing recording transcoding job for {path}", _targetPath);
|
||||
_logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,11 +318,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// TODO Investigate and properly fix.
|
||||
// Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading ffmpeg recording log");
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
internal class EpgChannelData
|
||||
{
|
||||
|
||||
private readonly Dictionary<string, ChannelInfo> _channelsById;
|
||||
|
||||
private readonly Dictionary<string, ChannelInfo> _channelsByNumber;
|
||||
|
||||
@@ -13,7 +13,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
/// <summary>
|
||||
/// Records the specified media source.
|
||||
/// </summary>
|
||||
Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
|
||||
/// <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,13 +1,12 @@
|
||||
#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;
|
||||
using MediaBrowser.Common.Json;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -23,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
}
|
||||
|
||||
public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
|
||||
public event EventHandler<GenericEventArgs<TimerInfo>>? TimerFired;
|
||||
|
||||
public void RestartTimers()
|
||||
{
|
||||
@@ -145,9 +143,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
private void TimerCallback(object state)
|
||||
private void TimerCallback(object? state)
|
||||
{
|
||||
var timerId = (string)state;
|
||||
var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state));
|
||||
|
||||
var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
|
||||
if (timer != null)
|
||||
@@ -156,12 +154,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
public TimerInfo GetTimer(string id)
|
||||
public TimerInfo? GetTimer(string id)
|
||||
{
|
||||
return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public TimerInfo GetTimerByProgramId(string programId)
|
||||
public TimerInfo? GetTimerByProgramId(string programId)
|
||||
{
|
||||
return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Broadcaster dto.
|
||||
/// </summary>
|
||||
public class BroadcasterDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the city.
|
||||
/// </summary>
|
||||
[JsonPropertyName("city")]
|
||||
public string? City { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public string? State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the postal code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("postalCode")]
|
||||
public string? Postalcode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the country.
|
||||
/// </summary>
|
||||
[JsonPropertyName("country")]
|
||||
public string? Country { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Caption dto.
|
||||
/// </summary>
|
||||
public class CaptionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the content.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lang.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lang")]
|
||||
public string? Lang { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Cast dto.
|
||||
/// </summary>
|
||||
public class CastDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the billing order.
|
||||
/// </summary>
|
||||
[JsonPropertyName("billingOrder")]
|
||||
public string? BillingOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("nameId")]
|
||||
public string? NameId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the person id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("personId")]
|
||||
public string? PersonId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the character name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("characterName")]
|
||||
public string? CharacterName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Channel dto.
|
||||
/// </summary>
|
||||
public class ChannelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of maps.
|
||||
/// </summary>
|
||||
[JsonPropertyName("map")]
|
||||
public IReadOnlyList<MapDto> Map { get; set; } = Array.Empty<MapDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of stations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stations")]
|
||||
public IReadOnlyList<StationDto> Stations { get; set; } = Array.Empty<StationDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public MetadataDto? Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Content rating dto.
|
||||
/// </summary>
|
||||
public class ContentRatingDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the body.
|
||||
/// </summary>
|
||||
[JsonPropertyName("body")]
|
||||
public string? Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public string? Code { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Crew dto.
|
||||
/// </summary>
|
||||
public class CrewDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the billing order.
|
||||
/// </summary>
|
||||
[JsonPropertyName("billingOrder")]
|
||||
public string? BillingOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("nameId")]
|
||||
public string? NameId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the person id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("personId")]
|
||||
public string? PersonId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Day dto.
|
||||
/// </summary>
|
||||
public class DayDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string? StationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of programs.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programs")]
|
||||
public IReadOnlyList<ProgramDto> Programs { get; set; } = Array.Empty<ProgramDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata schedule.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public MetadataScheduleDto? Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Description 1_000 dto.
|
||||
/// </summary>
|
||||
public class Description1000Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the description language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptionLanguage")]
|
||||
public string? DescriptionLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Description 100 dto.
|
||||
/// </summary>
|
||||
public class Description100Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the description language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptionLanguage")]
|
||||
public string? DescriptionLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Descriptions program dto.
|
||||
/// </summary>
|
||||
public class DescriptionsProgramDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of description 100.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description100")]
|
||||
public IReadOnlyList<Description100Dto> Description100 { get; set; } = Array.Empty<Description100Dto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of description1000.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description1000")]
|
||||
public IReadOnlyList<Description1000Dto> Description1000 { get; set; } = Array.Empty<Description1000Dto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Event details dto.
|
||||
/// </summary>
|
||||
public class EventDetailsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the sub type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("subType")]
|
||||
public string? SubType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Gracenote dto.
|
||||
/// </summary>
|
||||
public class GracenoteDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the season.
|
||||
/// </summary>
|
||||
[JsonPropertyName("season")]
|
||||
public int Season { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode.
|
||||
/// </summary>
|
||||
[JsonPropertyName("episode")]
|
||||
public int Episode { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Headends dto.
|
||||
/// </summary>
|
||||
public class HeadendsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the headend.
|
||||
/// </summary>
|
||||
[JsonPropertyName("headend")]
|
||||
public string? Headend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport")]
|
||||
public string? Transport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("location")]
|
||||
public string? Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of lineups.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineups")]
|
||||
public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Image data dto.
|
||||
/// </summary>
|
||||
public class ImageDataDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
[JsonPropertyName("width")]
|
||||
public string? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
[JsonPropertyName("height")]
|
||||
public string? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uri.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uri")]
|
||||
public string? Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
[JsonPropertyName("size")]
|
||||
public string? Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the aspect.
|
||||
/// </summary>
|
||||
[JsonPropertyName("aspect")]
|
||||
public string? Aspect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the category.
|
||||
/// </summary>
|
||||
[JsonPropertyName("category")]
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
public string? Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary.
|
||||
/// </summary>
|
||||
[JsonPropertyName("primary")]
|
||||
public string? Primary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tier")]
|
||||
public string? Tier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the caption.
|
||||
/// </summary>
|
||||
[JsonPropertyName("caption")]
|
||||
public CaptionDto? Caption { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// The lineup dto.
|
||||
/// </summary>
|
||||
public class LineupDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the linup.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineup")]
|
||||
public string? Lineup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lineup name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport")]
|
||||
public string? Transport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("location")]
|
||||
public string? Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uri.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uri")]
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Lineups dto.
|
||||
/// </summary>
|
||||
public class LineupsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the response code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("serverID")]
|
||||
public string? ServerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the datetime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("datetime")]
|
||||
public DateTime? LineupTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of lineups.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineups")]
|
||||
public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Logo dto.
|
||||
/// </summary>
|
||||
public class LogoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the url.
|
||||
/// </summary>
|
||||
[JsonPropertyName("URL")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
[JsonPropertyName("height")]
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
[JsonPropertyName("width")]
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the md5.
|
||||
/// </summary>
|
||||
[JsonPropertyName("md5")]
|
||||
public string? Md5 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Map dto.
|
||||
/// </summary>
|
||||
public class MapDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string? StationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channel.
|
||||
/// </summary>
|
||||
[JsonPropertyName("channel")]
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uhfvhf.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uhfVhf")]
|
||||
public int UhfVhf { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the atsc major.
|
||||
/// </summary>
|
||||
[JsonPropertyName("atscMajor")]
|
||||
public int AtscMajor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the atsc minor.
|
||||
/// </summary>
|
||||
[JsonPropertyName("atscMinor")]
|
||||
public int AtscMinor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the match type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("matchType")]
|
||||
public string? MatchType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata dto.
|
||||
/// </summary>
|
||||
public class MetadataDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the linup.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineup")]
|
||||
public string? Lineup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the modified timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("modified")]
|
||||
public string? Modified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport")]
|
||||
public string? Transport { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata programs dto.
|
||||
/// </summary>
|
||||
public class MetadataProgramsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the gracenote object.
|
||||
/// </summary>
|
||||
[JsonPropertyName("Gracenote")]
|
||||
public GracenoteDto? Gracenote { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata schedule dto.
|
||||
/// </summary>
|
||||
public class MetadataScheduleDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the modified timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("modified")]
|
||||
public string? Modified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the md5.
|
||||
/// </summary>
|
||||
[JsonPropertyName("md5")]
|
||||
public string? Md5 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("startDate")]
|
||||
public DateTime? StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the end date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("endDate")]
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the days count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("days")]
|
||||
public int Days { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Movie dto.
|
||||
/// </summary>
|
||||
public class MovieDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the year.
|
||||
/// </summary>
|
||||
[JsonPropertyName("year")]
|
||||
public string? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
[JsonPropertyName("duration")]
|
||||
public int Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of quality rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("qualityRating")]
|
||||
public IReadOnlyList<QualityRatingDto> QualityRating { get; set; } = Array.Empty<QualityRatingDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Multipart dto.
|
||||
/// </summary>
|
||||
public class MultipartDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the part number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("partNumber")]
|
||||
public int PartNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total parts.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalParts")]
|
||||
public int TotalParts { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Program details dto.
|
||||
/// </summary>
|
||||
public class ProgramDetailsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the audience.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audience")]
|
||||
public string? Audience { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the program id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programID")]
|
||||
public string? ProgramId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of titles.
|
||||
/// </summary>
|
||||
[JsonPropertyName("titles")]
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the descriptions.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptions")]
|
||||
public DescriptionsProgramDto? Descriptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the original air date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("originalAirDate")]
|
||||
public DateTime? OriginalAirDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of genres.
|
||||
/// </summary>
|
||||
[JsonPropertyName("genres")]
|
||||
public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("episodeTitle150")]
|
||||
public string? EpisodeTitle150 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public IReadOnlyList<MetadataProgramsDto> Metadata { get; set; } = Array.Empty<MetadataProgramsDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of content raitings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("contentRating")]
|
||||
public IReadOnlyList<ContentRatingDto> ContentRating { get; set; } = Array.Empty<ContentRatingDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of cast.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cast")]
|
||||
public IReadOnlyList<CastDto> Cast { get; set; } = Array.Empty<CastDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of crew.
|
||||
/// </summary>
|
||||
[JsonPropertyName("crew")]
|
||||
public IReadOnlyList<CrewDto> Crew { get; set; } = Array.Empty<CrewDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entity type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("entityType")]
|
||||
public string? EntityType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the show type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("showType")]
|
||||
public string? ShowType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether there is image artwork.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hasImageArtwork")]
|
||||
public bool HasImageArtwork { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("primaryImage")]
|
||||
public string? PrimaryImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the thumb image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("thumbImage")]
|
||||
public string? ThumbImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backdrop image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("backdropImage")]
|
||||
public string? BackdropImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the banner image.
|
||||
/// </summary>
|
||||
[JsonPropertyName("bannerImage")]
|
||||
public string? BannerImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("imageID")]
|
||||
public string? ImageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the md5.
|
||||
/// </summary>
|
||||
[JsonPropertyName("md5")]
|
||||
public string? Md5 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of content advisory.
|
||||
/// </summary>
|
||||
[JsonPropertyName("contentAdvisory")]
|
||||
public IReadOnlyList<string> ContentAdvisory { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the movie object.
|
||||
/// </summary>
|
||||
[JsonPropertyName("movie")]
|
||||
public MovieDto? Movie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of recommendations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("recommendations")]
|
||||
public IReadOnlyList<RecommendationDto> Recommendations { get; set; } = Array.Empty<RecommendationDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Program dto.
|
||||
/// </summary>
|
||||
public class ProgramDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the program id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programID")]
|
||||
public string? ProgramId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the air date time.
|
||||
/// </summary>
|
||||
[JsonPropertyName("airDateTime")]
|
||||
public DateTime? AirDateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
[JsonPropertyName("duration")]
|
||||
public int Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the md5.
|
||||
/// </summary>
|
||||
[JsonPropertyName("md5")]
|
||||
public string? Md5 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of audio properties.
|
||||
/// </summary>
|
||||
[JsonPropertyName("audioProperties")]
|
||||
public IReadOnlyList<string> AudioProperties { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of video properties.
|
||||
/// </summary>
|
||||
[JsonPropertyName("videoProperties")]
|
||||
public IReadOnlyList<string> VideoProperties { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of ratings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ratings")]
|
||||
public IReadOnlyList<RatingDto> Ratings { get; set; } = Array.Empty<RatingDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this program is new.
|
||||
/// </summary>
|
||||
[JsonPropertyName("new")]
|
||||
public bool? New { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the multipart object.
|
||||
/// </summary>
|
||||
[JsonPropertyName("multipart")]
|
||||
public MultipartDto? Multipart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the live tape delay.
|
||||
/// </summary>
|
||||
[JsonPropertyName("liveTapeDelay")]
|
||||
public string? LiveTapeDelay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this is the premiere.
|
||||
/// </summary>
|
||||
[JsonPropertyName("premiere")]
|
||||
public bool Premiere { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this is a repeat.
|
||||
/// </summary>
|
||||
[JsonPropertyName("repeat")]
|
||||
public bool Repeat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the premiere or finale.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isPremiereOrFinale")]
|
||||
public string? IsPremiereOrFinale { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Quality rating dto.
|
||||
/// </summary>
|
||||
public class QualityRatingDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ratings body.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ratingsBody")]
|
||||
public string? RatingsBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rating")]
|
||||
public string? Rating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the min rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("minRating")]
|
||||
public string? MinRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the max rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("maxRating")]
|
||||
public string? MaxRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the increment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("increment")]
|
||||
public string? Increment { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Rating dto.
|
||||
/// </summary>
|
||||
public class RatingDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the body.
|
||||
/// </summary>
|
||||
[JsonPropertyName("body")]
|
||||
public string? Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public string? Code { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Recommendation dto.
|
||||
/// </summary>
|
||||
public class RecommendationDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the program id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programID")]
|
||||
public string? ProgramId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title120")]
|
||||
public string? Title120 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Request schedule for channel dto.
|
||||
/// </summary>
|
||||
public class RequestScheduleForChannelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string? StationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of dates.
|
||||
/// </summary>
|
||||
[JsonPropertyName("date")]
|
||||
public IReadOnlyList<string> Date { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Show image dto.
|
||||
/// </summary>
|
||||
public class ShowImagesDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the program id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programID")]
|
||||
public string? ProgramId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of data.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public IReadOnlyList<ImageDataDto> Data { get; set; } = Array.Empty<ImageDataDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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; }
|
||||
|
||||
/// <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 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 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 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 a value indicating whether it is commercial free.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isCommercialFree")]
|
||||
public bool? IsCommercialFree { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Title dto.
|
||||
/// </summary>
|
||||
public class TitleDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title120")]
|
||||
public string? Title120 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// The token dto.
|
||||
/// </summary>
|
||||
public class TokenDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the response code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("serverID")]
|
||||
public string? ServerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the token.
|
||||
/// </summary>
|
||||
[JsonPropertyName("token")]
|
||||
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));
|
||||
|
||||
@@ -189,10 +190,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
IsSeries = program.Episode != null,
|
||||
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
|
||||
IsPremiere = program.Premiere != null,
|
||||
IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
|
||||
IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
|
||||
IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
|
||||
IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
|
||||
IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||
IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||
IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||
IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||
ImageUrl = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source) ? program.Icon.Source : null,
|
||||
HasImage = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source),
|
||||
OfficialRating = program.Rating != null && !string.IsNullOrEmpty(program.Rating.Value) ? program.Rating.Value : null,
|
||||
@@ -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();
|
||||
|
||||
@@ -7,12 +7,12 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@@ -161,7 +161,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(Series) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series },
|
||||
Name = seriesName,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Thumb },
|
||||
@@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
var program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
ExternalSeriesId = programSeriesId,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
@@ -255,7 +255,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(Series) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series },
|
||||
Name = seriesName,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Thumb },
|
||||
@@ -298,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
var program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(Series) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series },
|
||||
Name = seriesName,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
@@ -309,7 +309,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
ExternalSeriesId = programSeriesId,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
@@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting image info for {name}", info.Name);
|
||||
_logger.LogError(ex, "Error getting image info for {Name}", info.Name);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -33,8 +33,6 @@ using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
@@ -65,6 +63,8 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
|
||||
private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public LiveTvManager(
|
||||
IServerConfigurationManager config,
|
||||
ILogger<LiveTvManager> logger,
|
||||
@@ -189,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
IsKids = query.IsKids,
|
||||
IsSports = query.IsSports,
|
||||
IsSeries = query.IsSeries,
|
||||
IncludeItemTypes = new[] { nameof(LiveTvChannel) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
|
||||
TopParentIds = new[] { topFolder.Id },
|
||||
IsFavorite = query.IsFavorite,
|
||||
IsLiked = query.IsLiked,
|
||||
@@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
// Set the total bitrate if not already supplied
|
||||
mediaSource.InferTotalBitrate();
|
||||
|
||||
if (!(service is EmbyTV.EmbyTV))
|
||||
if (service is not EmbyTV.EmbyTV)
|
||||
{
|
||||
// We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says
|
||||
// mediaSource.SupportsDirectPlay = false;
|
||||
@@ -520,7 +520,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
return item;
|
||||
}
|
||||
|
||||
private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
|
||||
private (LiveTvProgram item, bool isNew, bool isUpdated) GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel)
|
||||
{
|
||||
var id = _tvDtoService.GetInternalProgramId(info.Id);
|
||||
|
||||
@@ -559,8 +559,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
item.ParentId = channel.Id;
|
||||
|
||||
// item.ChannelType = channelType;
|
||||
|
||||
item.Audio = info.Audio;
|
||||
item.ChannelId = channel.Id;
|
||||
item.CommunityRating ??= info.CommunityRating;
|
||||
@@ -772,7 +770,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
item.OnMetadataChanged();
|
||||
}
|
||||
|
||||
return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated);
|
||||
return (item, isNew, isUpdated);
|
||||
}
|
||||
|
||||
public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
|
||||
@@ -810,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
var internalQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
MinEndDate = query.MinEndDate,
|
||||
MinStartDate = query.MinStartDate,
|
||||
MaxEndDate = query.MaxEndDate,
|
||||
@@ -874,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
var internalQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
IsAiring = query.IsAiring,
|
||||
HasAired = query.HasAired,
|
||||
IsNews = query.IsNews,
|
||||
@@ -1054,7 +1052,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_logger.LogDebug("Refreshing guide from {name}", service.Name);
|
||||
_logger.LogDebug("Refreshing guide from {Name}", service.Name);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1085,8 +1083,8 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
if (cleanDatabase)
|
||||
{
|
||||
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
|
||||
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
|
||||
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { BaseItemKind.LiveTvChannel }, progress, cancellationToken);
|
||||
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { BaseItemKind.LiveTvProgram }, progress, cancellationToken);
|
||||
}
|
||||
|
||||
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
|
||||
@@ -1135,7 +1133,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting channel information for {name}", channelInfo.Item2.Name);
|
||||
_logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
@@ -1177,7 +1175,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
ChannelIds = new Guid[] { currentChannel.Id },
|
||||
DtoOptions = new DtoOptions(true)
|
||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||
@@ -1187,14 +1185,14 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
foreach (var program in channelPrograms)
|
||||
{
|
||||
var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken);
|
||||
var programItem = programTuple.Item1;
|
||||
var programTuple = GetProgram(program, existingPrograms, currentChannel);
|
||||
var programItem = programTuple.item;
|
||||
|
||||
if (programTuple.Item2)
|
||||
if (programTuple.isNew)
|
||||
{
|
||||
newPrograms.Add(programItem);
|
||||
}
|
||||
else if (programTuple.Item3)
|
||||
else if (programTuple.isUpdated)
|
||||
{
|
||||
updatedPrograms.Add(programItem);
|
||||
}
|
||||
@@ -1248,7 +1246,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting programs for channel {name}", currentChannel.Name);
|
||||
_logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
@@ -1261,7 +1259,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
return new Tuple<List<Guid>, List<Guid>>(channels, programs);
|
||||
}
|
||||
|
||||
private void CleanDatabaseInternal(Guid[] currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
private void CleanDatabaseInternal(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
|
||||
{
|
||||
@@ -1328,25 +1326,25 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
.Select(i => i.Id)
|
||||
.ToList();
|
||||
|
||||
var excludeItemTypes = new List<string>();
|
||||
var excludeItemTypes = new List<BaseItemKind>();
|
||||
|
||||
if (folderIds.Count == 0)
|
||||
{
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
|
||||
var includeItemTypes = new List<string>();
|
||||
var includeItemTypes = new List<BaseItemKind>();
|
||||
var genres = new List<string>();
|
||||
|
||||
if (query.IsMovie.HasValue)
|
||||
{
|
||||
if (query.IsMovie.Value)
|
||||
{
|
||||
includeItemTypes.Add(nameof(Movie));
|
||||
includeItemTypes.Add(BaseItemKind.Movie);
|
||||
}
|
||||
else
|
||||
{
|
||||
excludeItemTypes.Add(nameof(Movie));
|
||||
excludeItemTypes.Add(BaseItemKind.Movie);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1354,11 +1352,11 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
{
|
||||
if (query.IsSeries.Value)
|
||||
{
|
||||
includeItemTypes.Add(nameof(Episode));
|
||||
includeItemTypes.Add(BaseItemKind.Episode);
|
||||
}
|
||||
else
|
||||
{
|
||||
excludeItemTypes.Add(nameof(Episode));
|
||||
excludeItemTypes.Add(BaseItemKind.Episode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1385,10 +1383,10 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
// var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
|
||||
|
||||
// return new QueryResult<BaseItem>
|
||||
//{
|
||||
// {
|
||||
// Items = items,
|
||||
// TotalRecordCount = items.Length
|
||||
//};
|
||||
// };
|
||||
|
||||
dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray();
|
||||
}
|
||||
@@ -1425,16 +1423,15 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null)
|
||||
public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null)
|
||||
{
|
||||
var programTuples = new List<Tuple<BaseItemDto, string, string>>();
|
||||
var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
|
||||
var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo);
|
||||
|
||||
foreach (var tuple in tuples)
|
||||
foreach (var (item, dto) in programs)
|
||||
{
|
||||
var program = (LiveTvProgram)tuple.Item1;
|
||||
var dto = tuple.Item2;
|
||||
var program = (LiveTvProgram)item;
|
||||
|
||||
dto.StartDate = program.StartDate;
|
||||
dto.EpisodeTitle = program.EpisodeTitle;
|
||||
@@ -1724,7 +1721,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (!(service is EmbyTV.EmbyTV))
|
||||
if (service is not EmbyTV.EmbyTV)
|
||||
{
|
||||
TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
|
||||
}
|
||||
@@ -1871,15 +1868,15 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
return _libraryManager.GetItemById(internalChannelId);
|
||||
}
|
||||
|
||||
public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> tuples, DtoOptions options, User user)
|
||||
public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var channelIds = tuples.Select(i => i.Item2.Id).Distinct().ToArray();
|
||||
var channelIds = items.Select(i => i.Item2.Id).Distinct().ToArray();
|
||||
|
||||
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
||||
ChannelIds = channelIds,
|
||||
MaxStartDate = now,
|
||||
MinEndDate = now,
|
||||
@@ -1896,7 +1893,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
var addCurrentProgram = options.AddCurrentProgram;
|
||||
|
||||
foreach (var tuple in tuples)
|
||||
foreach (var tuple in items)
|
||||
{
|
||||
var dto = tuple.Item1;
|
||||
var channel = tuple.Item2;
|
||||
@@ -2050,7 +2047,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
_logger.LogInformation("New recording scheduled");
|
||||
|
||||
if (!(service is EmbyTV.EmbyTV))
|
||||
if (service is not EmbyTV.EmbyTV)
|
||||
{
|
||||
TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
|
||||
new TimerEventInfo(newTimerId)
|
||||
@@ -2118,17 +2115,13 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
@@ -2324,20 +2317,20 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
|
||||
}
|
||||
|
||||
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
|
||||
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
|
||||
{
|
||||
var config = GetConfiguration();
|
||||
|
||||
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
|
||||
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelId, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
|
||||
if (!string.Equals(tunerChannelId, providerChannelId, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var list = listingsProviderInfo.ChannelMappings.ToList();
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = tunerChannelId,
|
||||
Value = providerChannelId
|
||||
Name = tunerChannelNumber,
|
||||
Value = providerChannelNumber
|
||||
});
|
||||
listingsProviderInfo.ChannelMappings = list.ToArray();
|
||||
}
|
||||
@@ -2357,10 +2350,10 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
|
||||
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
|
||||
|
||||
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
|
||||
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> epgChannels)
|
||||
public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels)
|
||||
{
|
||||
var result = new TunerChannelMapping
|
||||
{
|
||||
@@ -2373,7 +2366,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
result.Name = tunerChannel.Number + " " + result.Name;
|
||||
}
|
||||
|
||||
var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels);
|
||||
var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels);
|
||||
|
||||
if (providerChannel != null)
|
||||
{
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||
// Dummy this up so that direct play checks can still run
|
||||
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
|
||||
{
|
||||
source.Path = _appHost.GetSmartApiUrl(string.Empty);
|
||||
source.Path = _appHost.GetApiUrlForLocalAccess();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -158,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
return new List<MediaSourceInfo>();
|
||||
}
|
||||
|
||||
protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tuner, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
|
||||
protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
|
||||
|
||||
public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,9 @@ using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -35,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;
|
||||
@@ -49,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)
|
||||
@@ -57,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_appHost = appHost;
|
||||
_socketFactory = socketFactory;
|
||||
_networkManager = networkManager;
|
||||
_streamHelper = streamHelper;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
@@ -69,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)
|
||||
@@ -89,22 +87,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
return lineup.Where(i => !i.DRM).ToList();
|
||||
}
|
||||
|
||||
private class HdHomerunChannelInfo : ChannelInfo
|
||||
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
|
||||
{
|
||||
public bool IsLegacyTuner { get; set; }
|
||||
}
|
||||
|
||||
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
|
||||
var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return lineup.Select(i => new HdHomerunChannelInfo
|
||||
{
|
||||
Name = i.GuideName,
|
||||
Number = i.GuideNumber,
|
||||
Id = GetChannelId(info, i),
|
||||
Id = GetChannelId(i),
|
||||
IsFavorite = i.Favorite,
|
||||
TunerHostId = info.Id,
|
||||
TunerHostId = tuner.Id,
|
||||
IsHD = i.HD,
|
||||
AudioCodec = i.AudioCodec,
|
||||
VideoCodec = i.VideoCodec,
|
||||
@@ -254,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));
|
||||
|
||||
@@ -263,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
|
||||
@@ -454,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,
|
||||
@@ -495,57 +488,53 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken)
|
||||
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<MediaSourceInfo>();
|
||||
|
||||
var channelId = channelInfo.Id;
|
||||
var channelId = channel.Id;
|
||||
var hdhrId = GetHdHrIdFromChannelId(channelId);
|
||||
|
||||
var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo;
|
||||
|
||||
var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner;
|
||||
|
||||
if (isLegacyTuner)
|
||||
if (channel is HdHomerunChannelInfo hdHomerunChannelInfo && hdHomerunChannelInfo.IsLegacyTuner)
|
||||
{
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
|
||||
var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (modelInfo != null && modelInfo.SupportsTranscoding)
|
||||
{
|
||||
if (info.AllowHWTranscoding)
|
||||
if (tuner.AllowHWTranscoding)
|
||||
{
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "heavy"));
|
||||
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "internet540"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "internet480"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "internet360"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "internet240"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "mobile"));
|
||||
}
|
||||
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
|
||||
list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
|
||||
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
|
||||
{
|
||||
var tunerCount = info.TunerCount;
|
||||
var tunerCount = tunerHost.TunerCount;
|
||||
|
||||
if (tunerCount > 0)
|
||||
{
|
||||
var tunerHostId = info.Id;
|
||||
var tunerHostId = tunerHost.Id;
|
||||
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (liveStreams.Count() >= tunerCount)
|
||||
@@ -554,28 +543,28 @@ 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}", channelInfo.Id, streamId, profile);
|
||||
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile);
|
||||
|
||||
var hdhrId = GetHdHrIdFromChannelId(channelInfo.Id);
|
||||
var hdhrId = GetHdHrIdFromChannelId(channel.Id);
|
||||
|
||||
var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
|
||||
var hdhomerunChannel = channel as HdHomerunChannelInfo;
|
||||
|
||||
var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
|
||||
var modelInfo = await GetModelInfo(tunerHost, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!modelInfo.SupportsTranscoding)
|
||||
{
|
||||
profile = "native";
|
||||
}
|
||||
|
||||
var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
|
||||
var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile);
|
||||
|
||||
if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
|
||||
{
|
||||
return new HdHomerunUdpStream(
|
||||
mediaSource,
|
||||
info,
|
||||
tunerHost,
|
||||
streamId,
|
||||
new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path),
|
||||
modelInfo.TunerCount,
|
||||
@@ -591,7 +580,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
mediaSource.Protocol = MediaProtocol.Http;
|
||||
|
||||
var httpUrl = channelInfo.Path;
|
||||
var httpUrl = channel.Path;
|
||||
|
||||
// If raw was used, the tuner doesn't support params
|
||||
if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -603,7 +592,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
return new SharedHttpStream(
|
||||
mediaSource,
|
||||
info,
|
||||
tunerHost,
|
||||
streamId,
|
||||
FileSystem,
|
||||
_httpClientFactory,
|
||||
@@ -615,7 +604,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
return new HdHomerunUdpStream(
|
||||
mediaSource,
|
||||
info,
|
||||
tunerHost,
|
||||
streamId,
|
||||
new HdHomerunChannelCommands(hdhomerunChannel.Number, profile),
|
||||
modelInfo.TunerCount,
|
||||
@@ -721,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,69 +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;
|
||||
@@ -116,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
client.Connect(remoteIp, HdHomeRunPort);
|
||||
await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false);
|
||||
|
||||
using var stream = client.GetStream();
|
||||
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
|
||||
@@ -149,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;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -82,7 +81,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
|
||||
|
||||
Logger.LogInformation("Opening HDHR UDP Live stream from {host}", uri.Host);
|
||||
Logger.LogInformation("Opening HDHR UDP Live stream from {Host}", uri.Host);
|
||||
|
||||
var remoteAddress = IPAddress.Parse(uri.Host);
|
||||
IPAddress localAddress;
|
||||
@@ -101,7 +100,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
if (localAddress.IsIPv4MappedToIPv6) {
|
||||
if (localAddress.IsIPv4MappedToIPv6)
|
||||
{
|
||||
localAddress = localAddress.MapToIPv4();
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
// OpenedMediaSource.Path = tempFile;
|
||||
// OpenedMediaSource.ReadAtNativeFramerate = true;
|
||||
|
||||
MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||
MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||
MediaSource.Protocol = MediaProtocol.Http;
|
||||
// OpenedMediaSource.SupportsDirectPlay = false;
|
||||
// OpenedMediaSource.SupportsDirectStream = true;
|
||||
@@ -156,11 +156,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 +179,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 +196,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)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -71,12 +72,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
|
||||
{
|
||||
var channelIdPrefix = GetFullChannelIdPrefix(info);
|
||||
var channelIdPrefix = GetFullChannelIdPrefix(tuner);
|
||||
|
||||
return await new M3uParser(Logger, _httpClientFactory)
|
||||
.Parse(info, channelIdPrefix, cancellationToken)
|
||||
.Parse(tuner, channelIdPrefix, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -96,13 +97,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
|
||||
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
|
||||
{
|
||||
var tunerCount = info.TunerCount;
|
||||
var tunerCount = tunerHost.TunerCount;
|
||||
|
||||
if (tunerCount > 0)
|
||||
{
|
||||
var tunerHostId = info.Id;
|
||||
var tunerHostId = tunerHost.Id;
|
||||
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (liveStreams.Count() >= tunerCount)
|
||||
@@ -111,7 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
}
|
||||
}
|
||||
|
||||
var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false);
|
||||
var sources = await GetChannelStreamMediaSources(tunerHost, channel, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var mediaSource = sources[0];
|
||||
|
||||
@@ -119,13 +120,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty;
|
||||
|
||||
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
|
||||
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
|
||||
}
|
||||
}
|
||||
|
||||
return new LiveStream(mediaSource, info, FileSystem, Logger, Config, _streamHelper);
|
||||
return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper);
|
||||
}
|
||||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
@@ -135,9 +136,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken)
|
||||
protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(info, channelInfo) });
|
||||
return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(tuner, channel) });
|
||||
}
|
||||
|
||||
protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel)
|
||||
|
||||
@@ -10,10 +10,11 @@ using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -43,22 +44,29 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
if (info == null)
|
||||
{
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
|
||||
if (!string.IsNullOrEmpty(info.UserAgent))
|
||||
{
|
||||
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
|
||||
}
|
||||
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(requestMessage, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new ArgumentNullException(nameof(info));
|
||||
}
|
||||
|
||||
return File.OpenRead(info.Url);
|
||||
if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return AsyncFile.OpenRead(info.Url);
|
||||
}
|
||||
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
|
||||
if (!string.IsNullOrEmpty(info.UserAgent))
|
||||
{
|
||||
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
|
||||
}
|
||||
|
||||
// Set HttpCompletionOption.ResponseHeadersRead to prevent timeouts on larger files
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
|
||||
@@ -82,7 +90,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
|
||||
_logger.LogInformation("Found m3u channel: {0}", extInf);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
|
||||
{
|
||||
@@ -98,6 +105,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
channel.Path = trimmedLine;
|
||||
channels.Add(channel);
|
||||
_logger.LogInformation("Parsed channel: {ChannelName}", channel.Name);
|
||||
extInf = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -230,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))
|
||||
{
|
||||
@@ -275,7 +283,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
// #EXTINF:0,84.0 - VOX Schweiz
|
||||
if (!string.IsNullOrWhiteSpace(nameInExtInf))
|
||||
{
|
||||
var numberIndex = nameInExtInf.IndexOf(' ');
|
||||
var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal);
|
||||
if (numberIndex > 0)
|
||||
{
|
||||
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
|
||||
@@ -288,11 +296,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
}
|
||||
}
|
||||
|
||||
attributes.TryGetValue("tvg-name", out string name);
|
||||
string name = nameInExtInf;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = nameInExtInf;
|
||||
attributes.TryGetValue("tvg-name", out name);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -97,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
// OpenedMediaSource.Path = tempFile;
|
||||
// OpenedMediaSource.ReadAtNativeFramerate = true;
|
||||
|
||||
MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||
MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||
MediaSource.Protocol = MediaProtocol.Http;
|
||||
|
||||
// OpenedMediaSource.Path = TempFilePath;
|
||||
@@ -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