fix delete old segments (#1536)
* code cleanup * ignore errors deleting old hls segments
This commit is contained in:
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Fix error loading path replacements when using MySql
|
||||
- Fix tray icon shortcut to open logs folder on Windows
|
||||
- Unlock playout when playout build fails
|
||||
- Ignore errors deleting old HLS segments; this should improve stream reliability
|
||||
|
||||
### Changed
|
||||
- Upgrade from .NET 7 to .NET 8
|
||||
|
||||
@@ -2,5 +2,10 @@ using ErsatzTV.Core.Iptv;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelPlaylist
|
||||
(string Scheme, string Host, string BaseUrl, string Mode, string UserAgent, string AccessToken) : IRequest<ChannelPlaylist>;
|
||||
public record GetChannelPlaylist(
|
||||
string Scheme,
|
||||
string Host,
|
||||
string BaseUrl,
|
||||
string Mode,
|
||||
string UserAgent,
|
||||
string AccessToken) : IRequest<ChannelPlaylist>;
|
||||
|
||||
@@ -40,7 +40,9 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, SynchronizeJellyfinCollections request)
|
||||
protected override async Task<DateTimeOffset> GetLastScan(
|
||||
TvContext dbContext,
|
||||
SynchronizeJellyfinCollections request)
|
||||
{
|
||||
DateTime minDateTime = await dbContext.JellyfinMediaSources
|
||||
.SelectOneAsync(l => l.Id, l => l.Id == request.JellyfinMediaSourceId)
|
||||
|
||||
@@ -2,5 +2,6 @@ using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Jellyfin;
|
||||
|
||||
public record SynchronizeJellyfinCollections(int JellyfinMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>,
|
||||
public record SynchronizeJellyfinCollections(int JellyfinMediaSourceId, bool ForceScan) :
|
||||
IRequest<Either<BaseError, Unit>>,
|
||||
IScannerBackgroundServiceRequest;
|
||||
|
||||
@@ -18,15 +18,15 @@ namespace ErsatzTV.Application.Streaming;
|
||||
|
||||
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IClient _client;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<StartFFmpegSessionHandler> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILogger<HlsSessionWorker> _sessionWorkerLogger;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||
|
||||
|
||||
@@ -22,23 +22,23 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
{
|
||||
private static readonly SemaphoreSlim Slim = new(1, 1);
|
||||
private static int _workAheadCount;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IClient _client;
|
||||
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<HlsSessionWorker> _logger;
|
||||
private readonly Option<int> _targetFramerate;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly object _sync = new();
|
||||
private readonly Option<int> _targetFramerate;
|
||||
private string _channelNumber;
|
||||
private bool _disposedValue;
|
||||
private bool _hasWrittenSegments;
|
||||
private DateTimeOffset _lastAccess;
|
||||
private DateTimeOffset _lastDelete = DateTimeOffset.MinValue;
|
||||
private IServiceScope _serviceScope;
|
||||
private HlsSessionState _state;
|
||||
private Timer _timer;
|
||||
private DateTimeOffset _transcodedUntil;
|
||||
private IServiceScope _serviceScope;
|
||||
|
||||
public HlsSessionWorker(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
@@ -511,9 +511,18 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
}
|
||||
|
||||
foreach (Segment segment in toDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(segment.File);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// work around lots of:
|
||||
// The process cannot access the file '...' because it is being used by another process
|
||||
_logger.LogDebug("Failed to delete old segment {File}", segment.File);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<long> GetPtsOffset(string channelNumber, CancellationToken cancellationToken)
|
||||
@@ -551,11 +560,9 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> GetWorkAheadLimit()
|
||||
{
|
||||
return await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters)
|
||||
private async Task<int> GetWorkAheadLimit() =>
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters)
|
||||
.Map(maybeCount => maybeCount.Match(identity, () => 1));
|
||||
}
|
||||
|
||||
private async Task<Option<string[]>> ReadPlaylistLines(CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -742,7 +742,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
|
||||
{ 10, TimeSpan.Parse("00:00:31.5791160") },
|
||||
{ 11, TimeSpan.Parse("00:00:31.2540360") },
|
||||
{ 12, TimeSpan.Parse("00:00:36.2231070") },
|
||||
{ 13, TimeSpan.Parse("00:02:00.0471430") },
|
||||
{ 13, TimeSpan.Parse("00:02:00.0471430") }
|
||||
});
|
||||
Collection collectionThree = TwoItemCollection(14, 15, TimeSpan.Parse("00:00:55.6349890"));
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ public abstract class ProgramScheduleItem
|
||||
public GuideMode GuideMode { get; set; }
|
||||
public string CustomTitle { get; set; }
|
||||
public int ProgramScheduleId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public ProgramSchedule ProgramSchedule { get; set; }
|
||||
|
||||
public int? CollectionId { get; set; }
|
||||
public Collection Collection { get; set; }
|
||||
public int? MediaItemId { get; set; }
|
||||
|
||||
@@ -9,9 +9,9 @@ public class ChannelPlaylist
|
||||
private readonly string _accessToken;
|
||||
private readonly string _baseUrl;
|
||||
private readonly List<Channel> _channels;
|
||||
private readonly string _userAgent;
|
||||
private readonly string _host;
|
||||
private readonly string _scheme;
|
||||
private readonly string _userAgent;
|
||||
|
||||
public ChannelPlaylist(
|
||||
string scheme,
|
||||
|
||||
@@ -428,7 +428,8 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
var collectionItemCount = collectionMediaItems.Map((k, v) => (k, v.Count)).Values.ToDictionary();
|
||||
|
||||
var scheduleItemsFillGroupEnumerators = new Dictionary<int, IScheduleItemsEnumerator>();
|
||||
foreach (ProgramScheduleItem scheduleItem in sortedScheduleItems.Where(si => si.FillWithGroupMode is not FillWithGroupMode.None))
|
||||
foreach (ProgramScheduleItem scheduleItem in sortedScheduleItems.Where(
|
||||
si => si.FillWithGroupMode is not FillWithGroupMode.None))
|
||||
{
|
||||
var collectionKey = CollectionKey.ForScheduleItem(scheduleItem);
|
||||
List<MediaItem> mediaItems = await MediaItemsForCollection.Collect(
|
||||
@@ -439,7 +440,8 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
var fakeCollections = _mediaCollectionRepository.GroupIntoFakeCollections(mediaItems)
|
||||
.Filter(c => c.ShowId > 0 || c.ArtistId > 0)
|
||||
.ToList();
|
||||
List<ProgramScheduleItem> fakeScheduleItems = [];
|
||||
List<ProgramScheduleItem> fakeScheduleItems = []
|
||||
;
|
||||
|
||||
// this will be used to clone a schedule item
|
||||
MethodInfo generic = typeof(JsonConvert).GetMethods()
|
||||
|
||||
@@ -11,10 +11,8 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
|
||||
private readonly Map<CollectionKey, int> _collectionItemCount;
|
||||
|
||||
public PlayoutModeSchedulerMultiple(Map<CollectionKey, int> collectionItemCount, ILogger logger)
|
||||
: base(logger)
|
||||
{
|
||||
: base(logger) =>
|
||||
_collectionItemCount = collectionItemCount;
|
||||
}
|
||||
|
||||
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
|
||||
@@ -15,7 +15,8 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
_mediaItems = mediaItems;
|
||||
_lazyMinimumDuration =
|
||||
new Lazy<Option<TimeSpan>>(() => _mediaItems.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
new Lazy<Option<TimeSpan>>(
|
||||
() => _mediaItems.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
_random = new Random(state.Seed);
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
|
||||
@@ -34,7 +34,8 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
_random = new Random(state.Seed);
|
||||
_shuffled = Shuffle(_collections, _random);
|
||||
_lazyMinimumDuration =
|
||||
new Lazy<Option<TimeSpan>>(() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
new Lazy<Option<TimeSpan>>(
|
||||
() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
while (State.Index < state.Index)
|
||||
|
||||
@@ -31,7 +31,8 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
_shuffled = Shuffle(_mediaItems, _random);
|
||||
_lazyMinimumDuration =
|
||||
new Lazy<Option<TimeSpan>>(() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
new Lazy<Option<TimeSpan>>(
|
||||
() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
while (State.Index < state.Index)
|
||||
|
||||
@@ -5,10 +5,10 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public class FFmpegCapabilities : IFFmpegCapabilities
|
||||
{
|
||||
private readonly IReadOnlySet<string> _ffmpegHardwareAccelerations;
|
||||
private readonly IReadOnlySet<string> _ffmpegDecoders;
|
||||
private readonly IReadOnlySet<string> _ffmpegEncoders;
|
||||
private readonly IReadOnlySet<string> _ffmpegFilters;
|
||||
private readonly IReadOnlySet<string> _ffmpegHardwareAccelerations;
|
||||
private readonly IReadOnlySet<string> _ffmpegOptions;
|
||||
|
||||
public FFmpegCapabilities(
|
||||
|
||||
@@ -2,13 +2,6 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public record FFmpegKnownDecoder
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
private FFmpegKnownDecoder(string Name)
|
||||
{
|
||||
this.Name = Name;
|
||||
}
|
||||
|
||||
public static readonly FFmpegKnownDecoder Av1Cuvid = new("av1_cuvid");
|
||||
public static readonly FFmpegKnownDecoder H264Cuvid = new("h264_cuvid");
|
||||
public static readonly FFmpegKnownDecoder HevcCuvid = new("hevc_cuvid");
|
||||
@@ -17,6 +10,10 @@ public record FFmpegKnownDecoder
|
||||
public static readonly FFmpegKnownDecoder Vc1Cuvid = new("vc1_cuvid");
|
||||
public static readonly FFmpegKnownDecoder Vp9Cuvid = new("vp9_cuvid");
|
||||
|
||||
private FFmpegKnownDecoder(string Name) => this.Name = Name;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public static IList<string> AllDecoders =>
|
||||
new[]
|
||||
{
|
||||
|
||||
@@ -2,12 +2,9 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public record FFmpegKnownEncoder
|
||||
{
|
||||
public string Name { get; }
|
||||
private FFmpegKnownEncoder(string Name) => this.Name = Name;
|
||||
|
||||
private FFmpegKnownEncoder(string Name)
|
||||
{
|
||||
this.Name = Name;
|
||||
}
|
||||
public string Name { get; }
|
||||
|
||||
// only list the encoders that we actually check for
|
||||
public static IList<string> AllEncoders =>
|
||||
|
||||
@@ -2,15 +2,12 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public record FFmpegKnownFilter
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
private FFmpegKnownFilter(string Name)
|
||||
{
|
||||
this.Name = Name;
|
||||
}
|
||||
|
||||
public static readonly FFmpegKnownFilter ScaleNpp = new("scale_npp");
|
||||
|
||||
private FFmpegKnownFilter(string Name) => this.Name = Name;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public static IList<string> AllFilters =>
|
||||
new[]
|
||||
{
|
||||
|
||||
@@ -2,19 +2,16 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public record FFmpegKnownHardwareAcceleration
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
private FFmpegKnownHardwareAcceleration(string Name)
|
||||
{
|
||||
this.Name = Name;
|
||||
}
|
||||
|
||||
public static readonly FFmpegKnownHardwareAcceleration Amf = new("amf");
|
||||
public static readonly FFmpegKnownHardwareAcceleration Cuda = new("cuda");
|
||||
public static readonly FFmpegKnownHardwareAcceleration Qsv = new("qsv");
|
||||
public static readonly FFmpegKnownHardwareAcceleration Vaapi = new("vaapi");
|
||||
public static readonly FFmpegKnownHardwareAcceleration VideoToolbox = new("videotoolbox");
|
||||
|
||||
private FFmpegKnownHardwareAcceleration(string Name) => this.Name = Name;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public static IList<string> AllAccels =>
|
||||
new[]
|
||||
{
|
||||
|
||||
@@ -6,15 +6,12 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||
public record FFmpegKnownOption
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
private FFmpegKnownOption(string Name)
|
||||
{
|
||||
this.Name = Name;
|
||||
}
|
||||
|
||||
public static readonly FFmpegKnownOption ReadrateInitialBurst = new("readrate_initial_burst");
|
||||
|
||||
private FFmpegKnownOption(string Name) => this.Name = Name;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public static IList<string> AllOptions =>
|
||||
new[]
|
||||
{
|
||||
|
||||
@@ -18,7 +18,10 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
||||
{
|
||||
private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture";
|
||||
private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model";
|
||||
private static readonly CompositeFormat VaapiCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}");
|
||||
|
||||
private static readonly CompositeFormat
|
||||
VaapiCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}");
|
||||
|
||||
private static readonly CompositeFormat QsvCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.qsv.{0}");
|
||||
private static readonly CompositeFormat FFmpegCapabilitiesCacheKeyFormat = CompositeFormat.Parse("ffmpeg.{0}");
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ public class DecoderImplicitCuda : DecoderBase
|
||||
{
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware;
|
||||
public override string Name => string.Empty;
|
||||
|
||||
public override string[] InputOptions(InputFile inputFile) =>
|
||||
new[]
|
||||
{
|
||||
|
||||
@@ -4,8 +4,8 @@ namespace ErsatzTV.FFmpeg.Filter;
|
||||
|
||||
public class CropFilter : BaseFilter
|
||||
{
|
||||
private readonly FrameState _currentState;
|
||||
private readonly FrameSize _croppedSize;
|
||||
private readonly FrameState _currentState;
|
||||
|
||||
public CropFilter(FrameState currentState, FrameSize croppedSize)
|
||||
{
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter.Cuda;
|
||||
|
||||
public class ScaleCudaFilter : BaseFilter
|
||||
{
|
||||
private readonly Option<FrameSize> _croppedSize;
|
||||
private readonly FrameState _currentState;
|
||||
private readonly bool _isAnamorphicEdgeCase;
|
||||
private readonly FrameSize _paddedSize;
|
||||
private readonly Option<FrameSize> _croppedSize;
|
||||
private readonly FrameSize _scaledSize;
|
||||
|
||||
public ScaleCudaFilter(
|
||||
|
||||
@@ -4,10 +4,7 @@ public class NormalizeLoudnessFilter : BaseFilter
|
||||
{
|
||||
private readonly AudioFilter _loudnessFilter;
|
||||
|
||||
public NormalizeLoudnessFilter(AudioFilter loudnessFilter)
|
||||
{
|
||||
_loudnessFilter = loudnessFilter;
|
||||
}
|
||||
public NormalizeLoudnessFilter(AudioFilter loudnessFilter) => _loudnessFilter = loudnessFilter;
|
||||
|
||||
public override string Filter => _loudnessFilter switch
|
||||
{
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter;
|
||||
|
||||
public class ScaleFilter : BaseFilter
|
||||
{
|
||||
private readonly Option<FrameSize> _croppedSize;
|
||||
private readonly FrameState _currentState;
|
||||
private readonly bool _isAnamorphicEdgeCase;
|
||||
private readonly FrameSize _paddedSize;
|
||||
private readonly Option<FrameSize> _croppedSize;
|
||||
private readonly FrameSize _scaledSize;
|
||||
|
||||
public ScaleFilter(
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter.Vaapi;
|
||||
|
||||
public class ScaleVaapiFilter : BaseFilter
|
||||
{
|
||||
private readonly Option<FrameSize> _croppedSize;
|
||||
private readonly FrameState _currentState;
|
||||
private readonly bool _isAnamorphicEdgeCase;
|
||||
private readonly FrameSize _paddedSize;
|
||||
private readonly Option<FrameSize> _croppedSize;
|
||||
private readonly FrameSize _scaledSize;
|
||||
|
||||
public ScaleVaapiFilter(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
public class VaapiSubtitlePixelFormatFilter : BaseFilter
|
||||
{
|
||||
public override FrameState NextState(FrameState currentState) => currentState;
|
||||
|
||||
public override string Filter => "format=vaapi|yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8";
|
||||
public override FrameState NextState(FrameState currentState) => currentState;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,8 @@ public class VideoFilter : IPipelineStep
|
||||
public FrameState NextState(FrameState currentState) => currentState;
|
||||
|
||||
private string[] Arguments() =>
|
||||
new []
|
||||
{
|
||||
[
|
||||
"-vf",
|
||||
string.Join(",", _filterSteps.Map(fs => fs.Filter))
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ public class OutputFormatHls : IPipelineStep
|
||||
{
|
||||
private readonly FrameState _desiredState;
|
||||
private readonly Option<string> _mediaFrameRate;
|
||||
private readonly string _playlistPath;
|
||||
private readonly bool _oneSecondGop;
|
||||
private readonly string _playlistPath;
|
||||
private readonly string _segmentTemplate;
|
||||
|
||||
public OutputFormatHls(
|
||||
|
||||
@@ -475,7 +475,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
|
||||
subtitle.FilterSteps.Add(subtitleHardwareUpload);
|
||||
|
||||
// only scale if scaling or padding was used for main video stream
|
||||
if (videoInputFile.FilterSteps.Any(s => s is ScaleFilter or ScaleCudaFilter { IsFormatOnly: false } or PadFilter))
|
||||
if (videoInputFile.FilterSteps.Any(
|
||||
s => s is ScaleFilter or ScaleCudaFilter { IsFormatOnly: false } or PadFilter))
|
||||
{
|
||||
var scaleFilter = new SubtitleScaleNppFilter(desiredState.PaddedSize);
|
||||
subtitle.FilterSteps.Add(scaleFilter);
|
||||
@@ -484,7 +485,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
|
||||
else
|
||||
{
|
||||
// only scale if scaling or padding was used for main video stream
|
||||
if (videoInputFile.FilterSteps.Any(s => s is ScaleFilter or ScaleCudaFilter { IsFormatOnly: false } or PadFilter))
|
||||
if (videoInputFile.FilterSteps.Any(
|
||||
s => s is ScaleFilter or ScaleCudaFilter { IsFormatOnly: false } or PadFilter))
|
||||
{
|
||||
var scaleFilter = new ScaleImageFilter(desiredState.PaddedSize);
|
||||
subtitle.FilterSteps.Add(scaleFilter);
|
||||
|
||||
@@ -72,7 +72,8 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
|
||||
reportsFolder,
|
||||
fontsFolder,
|
||||
_logger),
|
||||
HardwareAccelerationMode.VideoToolbox when capabilities is not NoHardwareCapabilities => new VideoToolboxPipelineBuilder(
|
||||
HardwareAccelerationMode.VideoToolbox when capabilities is not NoHardwareCapabilities => new
|
||||
VideoToolboxPipelineBuilder(
|
||||
ffmpegCapabilities,
|
||||
capabilities,
|
||||
hardwareAccelerationMode,
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations;
|
||||
|
||||
public class PlayoutScheduleItemFillGroupIndexConfiguration : IEntityTypeConfiguration<PlayoutScheduleItemFillGroupIndex>
|
||||
public class
|
||||
PlayoutScheduleItemFillGroupIndexConfiguration : IEntityTypeConfiguration<PlayoutScheduleItemFillGroupIndex>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlayoutScheduleItemFillGroupIndex> builder)
|
||||
{
|
||||
|
||||
@@ -14,8 +14,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin;
|
||||
|
||||
public class JellyfinApiClient : IJellyfinApiClient
|
||||
{
|
||||
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
|
||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
|
||||
private readonly ILogger<JellyfinApiClient> _logger;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ public class EntityLocker : IEntityLocker
|
||||
private readonly ConcurrentDictionary<Type, byte> _lockedRemoteMediaSourceTypes = new();
|
||||
private bool _embyCollections;
|
||||
private bool _jellyfinCollections;
|
||||
private bool _plexCollections;
|
||||
private bool _plex;
|
||||
private bool _plexCollections;
|
||||
private bool _trakt;
|
||||
|
||||
public event EventHandler OnLibraryChanged;
|
||||
|
||||
@@ -53,7 +53,9 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
return directory
|
||||
// .Filter(l => l.Hidden == 0)
|
||||
.Filter(l => l.Type.ToLowerInvariant() is "movie" or "show")
|
||||
.Filter(l => l.Type.ToLowerInvariant() is not "movie" || (l.Agent ?? string.Empty).ToLowerInvariant() is not "com.plexapp.agents.none")
|
||||
.Filter(
|
||||
l => l.Type.ToLowerInvariant() is not "movie" ||
|
||||
(l.Agent ?? string.Empty).ToLowerInvariant() is not "com.plexapp.agents.none")
|
||||
.Map(Project)
|
||||
.Somes()
|
||||
.ToList();
|
||||
@@ -407,7 +409,9 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
_ => None
|
||||
};
|
||||
|
||||
private Option<PlexCollection> ProjectToCollection(PlexMediaSource plexMediaSource, PlexCollectionMetadataResponse item)
|
||||
private Option<PlexCollection> ProjectToCollection(
|
||||
PlexMediaSource plexMediaSource,
|
||||
PlexCollectionMetadataResponse item)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -78,7 +78,8 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
_shuffled = Shuffle(_random);
|
||||
_lazyMinimumDuration =
|
||||
new Lazy<Option<TimeSpan>>(() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
new Lazy<Option<TimeSpan>>(
|
||||
() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
while (State.Index < state.Index)
|
||||
|
||||
@@ -809,7 +809,10 @@ public sealed class LuceneSearchIndex : ISearchIndex
|
||||
new StringField(IdField, musicVideo.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES),
|
||||
new StringField(TypeField, MusicVideoType, Field.Store.YES),
|
||||
new TextField(TitleField, metadata.Title ?? string.Empty, Field.Store.NO),
|
||||
new StringField(SortTitleField, (metadata.SortTitle ?? string.Empty).ToLowerInvariant(), Field.Store.NO),
|
||||
new StringField(
|
||||
SortTitleField,
|
||||
(metadata.SortTitle ?? string.Empty).ToLowerInvariant(),
|
||||
Field.Store.NO),
|
||||
new TextField(LibraryNameField, musicVideo.LibraryPath.Library.Name, Field.Store.NO),
|
||||
new StringField(
|
||||
LibraryIdField,
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
namespace ErsatzTV.Scanner.Application.Jellyfin;
|
||||
|
||||
public record SynchronizeJellyfinCollections(int JellyfinMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>;
|
||||
public record SynchronizeJellyfinCollections
|
||||
(int JellyfinMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
+1
-1
@@ -9,10 +9,10 @@ namespace ErsatzTV.Scanner.Application.Jellyfin;
|
||||
public class
|
||||
SynchronizeJellyfinCollectionsHandler : IRequestHandler<SynchronizeJellyfinCollections, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IJellyfinSecretStore _jellyfinSecretStore;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IJellyfinCollectionScanner _scanner;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public SynchronizeJellyfinCollectionsHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace ErsatzTV.Scanner.Application.Plex;
|
||||
public class SynchronizePlexCollectionsHandler : IRequestHandler<SynchronizePlexCollections, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
private readonly IPlexCollectionScanner _scanner;
|
||||
|
||||
public SynchronizePlexCollectionsHandler(
|
||||
|
||||
@@ -41,7 +41,7 @@ public class ShowNfoReader : NfoReader<ShowNfo>, IShowNfoReader
|
||||
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
|
||||
using var reader = XmlReader.Create(input, settings);
|
||||
var done = false;
|
||||
int showDepth = 0;
|
||||
var showDepth = 0;
|
||||
|
||||
while (!done && await reader.ReadAsync())
|
||||
{
|
||||
|
||||
@@ -131,7 +131,8 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
.HeadOrNone();
|
||||
|
||||
// skip folder if etag matches
|
||||
if (allFiles.Count == 0 || await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
|
||||
if (allFiles.Count == 0 ||
|
||||
await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
|
||||
etag)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -126,7 +126,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
||||
.HeadOrNone();
|
||||
|
||||
// skip folder if etag matches
|
||||
if (allFiles.Count == 0 || await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
|
||||
if (allFiles.Count == 0 ||
|
||||
await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
|
||||
etag)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -10,10 +10,10 @@ namespace ErsatzTV.Scanner.Core.Plex;
|
||||
|
||||
public class PlexCollectionScanner : IPlexCollectionScanner
|
||||
{
|
||||
private readonly IPlexServerApiClient _plexServerApiClient;
|
||||
private readonly IPlexCollectionRepository _plexCollectionRepository;
|
||||
private readonly ILogger<PlexCollectionScanner> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IPlexCollectionRepository _plexCollectionRepository;
|
||||
private readonly IPlexServerApiClient _plexServerApiClient;
|
||||
|
||||
public PlexCollectionScanner(
|
||||
IMediator mediator,
|
||||
|
||||
Reference in New Issue
Block a user