fix delete old segments (#1536)

* code cleanup

* ignore errors deleting old hls segments
This commit is contained in:
Jason Dove
2024-01-04 10:42:04 -06:00
committed by GitHub
parent 18ed20e203
commit c18be5559b
71 changed files with 207 additions and 187 deletions
+1
View File
@@ -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 error loading path replacements when using MySql
- Fix tray icon shortcut to open logs folder on Windows - Fix tray icon shortcut to open logs folder on Windows
- Unlock playout when playout build fails - Unlock playout when playout build fails
- Ignore errors deleting old HLS segments; this should improve stream reliability
### Changed ### Changed
- Upgrade from .NET 7 to .NET 8 - Upgrade from .NET 7 to .NET 8
@@ -2,5 +2,10 @@ using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels; namespace ErsatzTV.Application.Channels;
public record GetChannelPlaylist public record GetChannelPlaylist(
(string Scheme, string Host, string BaseUrl, string Mode, string UserAgent, string AccessToken) : IRequest<ChannelPlaylist>; string Scheme,
string Host,
string BaseUrl,
string Mode,
string UserAgent,
string AccessToken) : IRequest<ChannelPlaylist>;
@@ -22,7 +22,7 @@ public class
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory; _hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
} }
public async Task<List<HardwareAccelerationKind>> Handle( public async Task<List<HardwareAccelerationKind>> Handle(
GetSupportedHardwareAccelerationKinds request, GetSupportedHardwareAccelerationKinds request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -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 DateTime minDateTime = await dbContext.JellyfinMediaSources
.SelectOneAsync(l => l.Id, l => l.Id == request.JellyfinMediaSourceId) .SelectOneAsync(l => l.Id, l => l.Id == request.JellyfinMediaSourceId)
@@ -2,5 +2,6 @@ using ErsatzTV.Core;
namespace ErsatzTV.Application.Jellyfin; 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; IScannerBackgroundServiceRequest;
@@ -36,7 +36,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
return embyMediaSourceIds.Map(id => new LibraryViewModel("Emby", 0, "Collections", 0, id, string.Empty)); return embyMediaSourceIds.Map(id => new LibraryViewModel("Emby", 0, "Collections", 0, id, string.Empty));
} }
private static async Task<IEnumerable<LibraryViewModel>> GetJellyfinExternalCollections( private static async Task<IEnumerable<LibraryViewModel>> GetJellyfinExternalCollections(
TvContext dbContext, TvContext dbContext,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -49,7 +49,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
return jellyfinMediaSourceIds.Map( return jellyfinMediaSourceIds.Map(
id => new LibraryViewModel("Jellyfin", 0, "Collections", 0, id, string.Empty)); id => new LibraryViewModel("Jellyfin", 0, "Collections", 0, id, string.Empty));
} }
private static async Task<IEnumerable<LibraryViewModel>> GetPlexExternalCollections( private static async Task<IEnumerable<LibraryViewModel>> GetPlexExternalCollections(
TvContext dbContext, TvContext dbContext,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -18,15 +18,15 @@ namespace ErsatzTV.Application.Streaming;
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>> public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
{ {
private readonly IClient _client;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter; private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IMediator _mediator;
private readonly IClient _client;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<StartFFmpegSessionHandler> _logger; private readonly ILogger<StartFFmpegSessionHandler> _logger;
private readonly IMediator _mediator;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<HlsSessionWorker> _sessionWorkerLogger; private readonly ILogger<HlsSessionWorker> _sessionWorkerLogger;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel; private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
@@ -40,7 +40,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
ILogger<HlsSessionWorker> sessionWorkerLogger, ILogger<HlsSessionWorker> sessionWorkerLogger,
IFFmpegSegmenterService ffmpegSegmenterService, IFFmpegSegmenterService ffmpegSegmenterService,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
IHostApplicationLifetime hostApplicationLifetime, IHostApplicationLifetime hostApplicationLifetime,
ChannelWriter<IBackgroundServiceRequest> workerChannel) ChannelWriter<IBackgroundServiceRequest> workerChannel)
{ {
_hlsPlaylistFilter = hlsPlaylistFilter; _hlsPlaylistFilter = hlsPlaylistFilter;
@@ -73,7 +73,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
Option<int> targetFramerate = await _mediator.Send( Option<int> targetFramerate = await _mediator.Send(
new GetChannelFramerate(request.ChannelNumber), new GetChannelFramerate(request.ChannelNumber),
cancellationToken); cancellationToken);
var worker = new HlsSessionWorker( var worker = new HlsSessionWorker(
_serviceScopeFactory, _serviceScopeFactory,
_client, _client,
@@ -92,7 +92,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_ffmpegSegmenterService.SessionWorkers.TryRemove( _ffmpegSegmenterService.SessionWorkers.TryRemove(
request.ChannelNumber, request.ChannelNumber,
out IHlsSessionWorker inactiveWorker); out IHlsSessionWorker inactiveWorker);
inactiveWorker?.Dispose(); inactiveWorker?.Dispose();
_workerChannel.TryWrite(new ReleaseMemory(false)); _workerChannel.TryWrite(new ReleaseMemory(false));
@@ -22,23 +22,23 @@ public class HlsSessionWorker : IHlsSessionWorker
{ {
private static readonly SemaphoreSlim Slim = new(1, 1); private static readonly SemaphoreSlim Slim = new(1, 1);
private static int _workAheadCount; private static int _workAheadCount;
private readonly IMediator _mediator;
private readonly IClient _client; private readonly IClient _client;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<HlsSessionWorker> _logger; private readonly ILogger<HlsSessionWorker> _logger;
private readonly Option<int> _targetFramerate; private readonly IMediator _mediator;
private readonly object _sync = new(); private readonly object _sync = new();
private readonly Option<int> _targetFramerate;
private string _channelNumber; private string _channelNumber;
private bool _disposedValue; private bool _disposedValue;
private bool _hasWrittenSegments; private bool _hasWrittenSegments;
private DateTimeOffset _lastAccess; private DateTimeOffset _lastAccess;
private DateTimeOffset _lastDelete = DateTimeOffset.MinValue; private DateTimeOffset _lastDelete = DateTimeOffset.MinValue;
private IServiceScope _serviceScope;
private HlsSessionState _state; private HlsSessionState _state;
private Timer _timer; private Timer _timer;
private DateTimeOffset _transcodedUntil; private DateTimeOffset _transcodedUntil;
private IServiceScope _serviceScope;
public HlsSessionWorker( public HlsSessionWorker(
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
@@ -223,7 +223,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{ {
_timer.Dispose(); _timer.Dispose();
_timer = null; _timer = null;
_serviceScope.Dispose(); _serviceScope.Dispose();
_serviceScope = null; _serviceScope = null;
} }
@@ -278,14 +278,14 @@ public class HlsSessionWorker : IHlsSessionWorker
{ {
Interlocked.Increment(ref _workAheadCount); Interlocked.Increment(ref _workAheadCount);
_logger.LogInformation("HLS segmenter will work ahead for channel {Channel}", _channelNumber); _logger.LogInformation("HLS segmenter will work ahead for channel {Channel}", _channelNumber);
HlsSessionState nextState = _state switch HlsSessionState nextState = _state switch
{ {
HlsSessionState.SeekAndRealtime => HlsSessionState.SeekAndWorkAhead, HlsSessionState.SeekAndRealtime => HlsSessionState.SeekAndWorkAhead,
HlsSessionState.ZeroAndRealtime => HlsSessionState.ZeroAndWorkAhead, HlsSessionState.ZeroAndRealtime => HlsSessionState.ZeroAndWorkAhead,
_ => _state _ => _state
}; };
if (nextState != _state) if (nextState != _state)
{ {
_logger.LogDebug("HLS session state accelerating {Last} => {Next}", _state, nextState); _logger.LogDebug("HLS session state accelerating {Last} => {Next}", _state, nextState);
@@ -512,7 +512,16 @@ public class HlsSessionWorker : IHlsSessionWorker
foreach (Segment segment in toDelete) foreach (Segment segment in toDelete)
{ {
File.Delete(segment.File); 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);
}
} }
} }
@@ -551,11 +560,9 @@ public class HlsSessionWorker : IHlsSessionWorker
} }
} }
private async Task<int> GetWorkAheadLimit() private async Task<int> GetWorkAheadLimit() =>
{ await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters)
return await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters)
.Map(maybeCount => maybeCount.Match(identity, () => 1)); .Map(maybeCount => maybeCount.Match(identity, () => 1));
}
private async Task<Option<string[]>> ReadPlaylistLines(CancellationToken cancellationToken) private async Task<Option<string[]>> ReadPlaylistLines(CancellationToken cancellationToken)
{ {
@@ -17,7 +17,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
private readonly ILogger<PlayoutModeSchedulerDuration> _logger; private readonly ILogger<PlayoutModeSchedulerDuration> _logger;
public PlayoutModeSchedulerDurationTests() public PlayoutModeSchedulerDurationTests()
{ {
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
@@ -725,7 +725,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
playoutItems[6].GuideFinish.HasValue.Should().BeFalse(); playoutItems[6].GuideFinish.HasValue.Should().BeFalse();
} }
[Test] [Test]
public void Should_Not_Have_Gap_With_Post_Roll_Pad_And_Fallback_Filler() public void Should_Not_Have_Gap_With_Post_Roll_Pad_And_Fallback_Filler()
{ {
@@ -742,7 +742,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
{ 10, TimeSpan.Parse("00:00:31.5791160") }, { 10, TimeSpan.Parse("00:00:31.5791160") },
{ 11, TimeSpan.Parse("00:00:31.2540360") }, { 11, TimeSpan.Parse("00:00:31.2540360") },
{ 12, TimeSpan.Parse("00:00:36.2231070") }, { 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")); Collection collectionThree = TwoItemCollection(14, 15, TimeSpan.Parse("00:00:55.6349890"));
@@ -820,7 +820,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddMinutes(30)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddMinutes(30));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
// THIS IS THE KEY TEST - needs to be exactly 30 minutes // THIS IS THE KEY TEST - needs to be exactly 30 minutes
(playoutItems.Last().FinishOffset - playoutItems.First().StartOffset).Should().Be(TimeSpan.FromMinutes(30)); (playoutItems.Last().FinishOffset - playoutItems.First().StartOffset).Should().Be(TimeSpan.FromMinutes(30));
@@ -45,7 +45,7 @@ public abstract class SchedulerTestBase
{ CollectionKey.ForFillerPreset(fillerPreset), enumerator2 }, { CollectionKey.ForFillerPreset(fillerPreset), enumerator2 },
{ CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 } { CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 }
}; };
protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators( protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators(
ProgramScheduleItem scheduleItem, ProgramScheduleItem scheduleItem,
IMediaCollectionEnumerator enumerator1, IMediaCollectionEnumerator enumerator1,
@@ -98,7 +98,7 @@ public abstract class SchedulerTestBase
TestMovie(id2, duration, new DateTime(2020, 1, 2), chapterCount) TestMovie(id2, duration, new DateTime(2020, 1, 2), chapterCount)
} }
}; };
protected static Collection CollectionOf(IDictionary<int, TimeSpan> idsAndDurations, int chapterCount = 0) protected static Collection CollectionOf(IDictionary<int, TimeSpan> idsAndDurations, int chapterCount = 0)
{ {
var mediaItems = new List<MediaItem>(); var mediaItems = new List<MediaItem>();
@@ -10,9 +10,9 @@ public class PlayoutScheduleItemFillGroupIndex
[NotLogged] [NotLogged]
public Playout Playout { get; set; } public Playout Playout { get; set; }
public int ProgramScheduleItemId { get; set; } public int ProgramScheduleItemId { get; set; }
[NotLogged] [NotLogged]
public ProgramScheduleItem ProgramScheduleItem { get; set; } public ProgramScheduleItem ProgramScheduleItem { get; set; }
@@ -13,8 +13,10 @@ public abstract class ProgramScheduleItem
public GuideMode GuideMode { get; set; } public GuideMode GuideMode { get; set; }
public string CustomTitle { get; set; } public string CustomTitle { get; set; }
public int ProgramScheduleId { get; set; } public int ProgramScheduleId { get; set; }
[JsonIgnore] [JsonIgnore]
public ProgramSchedule ProgramSchedule { get; set; } public ProgramSchedule ProgramSchedule { get; set; }
public int? CollectionId { get; set; } public int? CollectionId { get; set; }
public Collection Collection { get; set; } public Collection Collection { get; set; }
public int? MediaItemId { get; set; } public int? MediaItemId { get; set; }
@@ -329,7 +329,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Width,
channel.FFmpegProfile.Resolution.Height); channel.FFmpegProfile.Resolution.Height);
} }
var desiredState = new FrameState( var desiredState = new FrameState(
playbackSettings.RealtimeOutput, playbackSettings.RealtimeOutput,
fillerKind == FillerKind.Fallback, fillerKind == FillerKind.Fallback,
@@ -67,7 +67,7 @@ public interface IPlexServerApiClient
PlexLibrary library, PlexLibrary library,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token); PlexServerAuthToken token);
IAsyncEnumerable<PlexCollection> GetAllCollections( IAsyncEnumerable<PlexCollection> GetAllCollections(
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
+2 -2
View File
@@ -9,9 +9,9 @@ public class ChannelPlaylist
private readonly string _accessToken; private readonly string _accessToken;
private readonly string _baseUrl; private readonly string _baseUrl;
private readonly List<Channel> _channels; private readonly List<Channel> _channels;
private readonly string _userAgent;
private readonly string _host; private readonly string _host;
private readonly string _scheme; private readonly string _scheme;
private readonly string _userAgent;
public ChannelPlaylist( public ChannelPlaylist(
string scheme, string scheme,
@@ -58,7 +58,7 @@ public class ChannelPlaylist
sb.AppendLine("#KODIPROP:inputstream.ffmpegdirect.open_mode=ffmpeg"); sb.AppendLine("#KODIPROP:inputstream.ffmpegdirect.open_mode=ffmpeg");
} }
string logo = Optional(channel.Artwork).Flatten() string logo = Optional(channel.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Logo) .Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone() .HeadOrNone()
+14 -12
View File
@@ -424,11 +424,12 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken); cancellationToken);
collectionEnumerators.Add(collectionKey, enumerator); collectionEnumerators.Add(collectionKey, enumerator);
} }
var collectionItemCount = collectionMediaItems.Map((k, v) => (k, v.Count)).Values.ToDictionary(); var collectionItemCount = collectionMediaItems.Map((k, v) => (k, v.Count)).Values.ToDictionary();
var scheduleItemsFillGroupEnumerators = new Dictionary<int, IScheduleItemsEnumerator>(); 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); var collectionKey = CollectionKey.ForScheduleItem(scheduleItem);
List<MediaItem> mediaItems = await MediaItemsForCollection.Collect( List<MediaItem> mediaItems = await MediaItemsForCollection.Collect(
@@ -439,14 +440,15 @@ public class PlayoutBuilder : IPlayoutBuilder
var fakeCollections = _mediaCollectionRepository.GroupIntoFakeCollections(mediaItems) var fakeCollections = _mediaCollectionRepository.GroupIntoFakeCollections(mediaItems)
.Filter(c => c.ShowId > 0 || c.ArtistId > 0) .Filter(c => c.ShowId > 0 || c.ArtistId > 0)
.ToList(); .ToList();
List<ProgramScheduleItem> fakeScheduleItems = []; List<ProgramScheduleItem> fakeScheduleItems = []
;
// this will be used to clone a schedule item // this will be used to clone a schedule item
MethodInfo generic = typeof(JsonConvert).GetMethods() MethodInfo generic = typeof(JsonConvert).GetMethods()
.FirstOrDefault( .FirstOrDefault(
x => x.Name.Equals("DeserializeObject", StringComparison.OrdinalIgnoreCase) && x.IsGenericMethod && x => x.Name.Equals("DeserializeObject", StringComparison.OrdinalIgnoreCase) && x.IsGenericMethod &&
x.GetParameters().Length == 1)?.MakeGenericMethod(scheduleItem.GetType()); x.GetParameters().Length == 1)?.MakeGenericMethod(scheduleItem.GetType());
foreach (CollectionWithItems fakeCollection in fakeCollections) foreach (CollectionWithItems fakeCollection in fakeCollections)
{ {
var key = new CollectionKey var key = new CollectionKey
@@ -473,7 +475,7 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken); cancellationToken);
collectionEnumerators.Add(key, enumerator); collectionEnumerators.Add(key, enumerator);
// this makes multiple (0) work - since it needs the number of items in the collection // this makes multiple (0) work - since it needs the number of items in the collection
collectionItemCount.Add(key, fakeCollection.MediaItems.Count); collectionItemCount.Add(key, fakeCollection.MediaItems.Count);
} }
@@ -572,7 +574,7 @@ public class PlayoutBuilder : IPlayoutBuilder
// get the schedule item out of the sorted list // get the schedule item out of the sorted list
ProgramScheduleItem scheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current; ProgramScheduleItem scheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;
// replace with the fake schedule item when filling with group // replace with the fake schedule item when filling with group
if (scheduleItem.FillWithGroupMode is not FillWithGroupMode.None) if (scheduleItem.FillWithGroupMode is not FillWithGroupMode.None)
{ {
@@ -672,7 +674,7 @@ public class PlayoutBuilder : IPlayoutBuilder
activeSchedule, activeSchedule,
collectionEnumerators, collectionEnumerators,
saveAnchorDate); saveAnchorDate);
// build fill group indices // build fill group indices
playout.FillGroupIndices = BuildFillGroupIndices(playout, scheduleItemsFillGroupEnumerators); playout.FillGroupIndices = BuildFillGroupIndices(playout, scheduleItemsFillGroupEnumerators);
@@ -684,7 +686,7 @@ public class PlayoutBuilder : IPlayoutBuilder
Dictionary<int, IScheduleItemsEnumerator> scheduleItemsFillGroupEnumerators) Dictionary<int, IScheduleItemsEnumerator> scheduleItemsFillGroupEnumerators)
{ {
var result = playout.FillGroupIndices.ToList(); var result = playout.FillGroupIndices.ToList();
foreach ((int programScheduleItemId, IScheduleItemsEnumerator enumerator) in scheduleItemsFillGroupEnumerators) foreach ((int programScheduleItemId, IScheduleItemsEnumerator enumerator) in scheduleItemsFillGroupEnumerators)
{ {
Option<PlayoutScheduleItemFillGroupIndex> maybeFgi = Optional( Option<PlayoutScheduleItemFillGroupIndex> maybeFgi = Optional(
@@ -694,7 +696,7 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
fgi.EnumeratorState = enumerator.State; fgi.EnumeratorState = enumerator.State;
} }
if (maybeFgi.IsNone) if (maybeFgi.IsNone)
{ {
var fgi = new PlayoutScheduleItemFillGroupIndex var fgi = new PlayoutScheduleItemFillGroupIndex
@@ -703,7 +705,7 @@ public class PlayoutBuilder : IPlayoutBuilder
ProgramScheduleItemId = programScheduleItemId, ProgramScheduleItemId = programScheduleItemId,
EnumeratorState = enumerator.State EnumeratorState = enumerator.State
}; };
result.Add(fgi); result.Add(fgi);
} }
} }
@@ -223,7 +223,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
Logger.LogError("Multiple pad-to-nearest-minute values are invalid; no filler will be used"); Logger.LogError("Multiple pad-to-nearest-minute values are invalid; no filler will be used");
return new List<PlayoutItem> { playoutItem }; return new List<PlayoutItem> { playoutItem };
} }
// missing pad-to-nearest-minute value is invalid; use no filler // missing pad-to-nearest-minute value is invalid; use no filler
FillerPreset invalidPadFiller = allFiller FillerPreset invalidPadFiller = allFiller
.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue == false); .FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue == false);
@@ -11,10 +11,8 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
private readonly Map<CollectionKey, int> _collectionItemCount; private readonly Map<CollectionKey, int> _collectionItemCount;
public PlayoutModeSchedulerMultiple(Map<CollectionKey, int> collectionItemCount, ILogger logger) public PlayoutModeSchedulerMultiple(Map<CollectionKey, int> collectionItemCount, ILogger logger)
: base(logger) : base(logger) =>
{
_collectionItemCount = collectionItemCount; _collectionItemCount = collectionItemCount;
}
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule( public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
PlayoutBuilderState playoutBuilderState, PlayoutBuilderState playoutBuilderState,
@@ -15,7 +15,8 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
{ {
_mediaItems = mediaItems; _mediaItems = mediaItems;
_lazyMinimumDuration = _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); _random = new Random(state.Seed);
State = new CollectionEnumeratorState { Seed = state.Seed }; State = new CollectionEnumeratorState { Seed = state.Seed };
@@ -34,7 +34,8 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
_random = new Random(state.Seed); _random = new Random(state.Seed);
_shuffled = Shuffle(_collections, _random); _shuffled = Shuffle(_collections, _random);
_lazyMinimumDuration = _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 }; State = new CollectionEnumeratorState { Seed = state.Seed };
while (State.Index < state.Index) while (State.Index < state.Index)
@@ -31,7 +31,8 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
_random = new CloneableRandom(state.Seed); _random = new CloneableRandom(state.Seed);
_shuffled = Shuffle(_mediaItems, _random); _shuffled = Shuffle(_mediaItems, _random);
_lazyMinimumDuration = _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 }; State = new CollectionEnumeratorState { Seed = state.Seed };
while (State.Index < state.Index) while (State.Index < state.Index)
@@ -5,10 +5,10 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public class FFmpegCapabilities : IFFmpegCapabilities public class FFmpegCapabilities : IFFmpegCapabilities
{ {
private readonly IReadOnlySet<string> _ffmpegHardwareAccelerations;
private readonly IReadOnlySet<string> _ffmpegDecoders; private readonly IReadOnlySet<string> _ffmpegDecoders;
private readonly IReadOnlySet<string> _ffmpegEncoders; private readonly IReadOnlySet<string> _ffmpegEncoders;
private readonly IReadOnlySet<string> _ffmpegFilters; private readonly IReadOnlySet<string> _ffmpegFilters;
private readonly IReadOnlySet<string> _ffmpegHardwareAccelerations;
private readonly IReadOnlySet<string> _ffmpegOptions; private readonly IReadOnlySet<string> _ffmpegOptions;
public FFmpegCapabilities( public FFmpegCapabilities(
@@ -32,8 +32,8 @@ public class FFmpegCapabilities : IFFmpegCapabilities
{ {
return _ffmpegEncoders.Any( return _ffmpegEncoders.Any(
e => e.EndsWith($"_{FFmpegKnownHardwareAcceleration.Amf.Name}", StringComparison.OrdinalIgnoreCase)); e => e.EndsWith($"_{FFmpegKnownHardwareAcceleration.Amf.Name}", StringComparison.OrdinalIgnoreCase));
} }
Option<FFmpegKnownHardwareAcceleration> maybeAccelToCheck = hardwareAccelerationMode switch Option<FFmpegKnownHardwareAcceleration> maybeAccelToCheck = hardwareAccelerationMode switch
{ {
HardwareAccelerationMode.Nvenc => FFmpegKnownHardwareAcceleration.Cuda, HardwareAccelerationMode.Nvenc => FFmpegKnownHardwareAcceleration.Cuda,
@@ -2,13 +2,6 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public record FFmpegKnownDecoder 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 Av1Cuvid = new("av1_cuvid");
public static readonly FFmpegKnownDecoder H264Cuvid = new("h264_cuvid"); public static readonly FFmpegKnownDecoder H264Cuvid = new("h264_cuvid");
public static readonly FFmpegKnownDecoder HevcCuvid = new("hevc_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 Vc1Cuvid = new("vc1_cuvid");
public static readonly FFmpegKnownDecoder Vp9Cuvid = new("vp9_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 => public static IList<string> AllDecoders =>
new[] new[]
{ {
@@ -2,12 +2,9 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public record FFmpegKnownEncoder public record FFmpegKnownEncoder
{ {
public string Name { get; } private FFmpegKnownEncoder(string Name) => this.Name = Name;
private FFmpegKnownEncoder(string Name) public string Name { get; }
{
this.Name = Name;
}
// only list the encoders that we actually check for // only list the encoders that we actually check for
public static IList<string> AllEncoders => public static IList<string> AllEncoders =>
@@ -2,15 +2,12 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public record FFmpegKnownFilter public record FFmpegKnownFilter
{ {
public string Name { get; }
private FFmpegKnownFilter(string Name)
{
this.Name = Name;
}
public static readonly FFmpegKnownFilter ScaleNpp = new("scale_npp"); public static readonly FFmpegKnownFilter ScaleNpp = new("scale_npp");
private FFmpegKnownFilter(string Name) => this.Name = Name;
public string Name { get; }
public static IList<string> AllFilters => public static IList<string> AllFilters =>
new[] new[]
{ {
@@ -2,19 +2,16 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public record FFmpegKnownHardwareAcceleration 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 Amf = new("amf");
public static readonly FFmpegKnownHardwareAcceleration Cuda = new("cuda"); public static readonly FFmpegKnownHardwareAcceleration Cuda = new("cuda");
public static readonly FFmpegKnownHardwareAcceleration Qsv = new("qsv"); public static readonly FFmpegKnownHardwareAcceleration Qsv = new("qsv");
public static readonly FFmpegKnownHardwareAcceleration Vaapi = new("vaapi"); public static readonly FFmpegKnownHardwareAcceleration Vaapi = new("vaapi");
public static readonly FFmpegKnownHardwareAcceleration VideoToolbox = new("videotoolbox"); public static readonly FFmpegKnownHardwareAcceleration VideoToolbox = new("videotoolbox");
private FFmpegKnownHardwareAcceleration(string Name) => this.Name = Name;
public string Name { get; }
public static IList<string> AllAccels => public static IList<string> AllAccels =>
new[] new[]
{ {
@@ -6,15 +6,12 @@ namespace ErsatzTV.FFmpeg.Capabilities;
[SuppressMessage("ReSharper", "StringLiteralTypo")] [SuppressMessage("ReSharper", "StringLiteralTypo")]
public record FFmpegKnownOption public record FFmpegKnownOption
{ {
public string Name { get; }
private FFmpegKnownOption(string Name)
{
this.Name = Name;
}
public static readonly FFmpegKnownOption ReadrateInitialBurst = new("readrate_initial_burst"); 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 => public static IList<string> AllOptions =>
new[] new[]
{ {
@@ -18,7 +18,10 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
{ {
private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture"; private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture";
private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model"; 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 QsvCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.qsv.{0}");
private static readonly CompositeFormat FFmpegCapabilitiesCacheKeyFormat = CompositeFormat.Parse("ffmpeg.{0}"); private static readonly CompositeFormat FFmpegCapabilitiesCacheKeyFormat = CompositeFormat.Parse("ffmpeg.{0}");
@@ -86,7 +89,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
{ {
return new NoHardwareCapabilities(); return new NoHardwareCapabilities();
} }
if (!ffmpegCapabilities.HasHardwareAcceleration(hardwareAccelerationMode)) if (!ffmpegCapabilities.HasHardwareAcceleration(hardwareAccelerationMode))
{ {
_logger.LogWarning( _logger.LogWarning(
@@ -95,7 +98,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return new NoHardwareCapabilities(); return new NoHardwareCapabilities();
} }
return hardwareAccelerationMode switch return hardwareAccelerationMode switch
{ {
HardwareAccelerationMode.Nvenc => await GetNvidiaCapabilities(ffmpegPath, ffmpegCapabilities), HardwareAccelerationMode.Nvenc => await GetNvidiaCapabilities(ffmpegPath, ffmpegCapabilities),
@@ -135,7 +138,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
var arguments = option.GlobalOptions.ToList(); var arguments = option.GlobalOptions.ToList();
arguments.AddRange(QsvArguments); arguments.AddRange(QsvArguments);
BufferedCommandResult result = await Cli.Wrap(ffmpegPath) BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithArguments(arguments) .WithArguments(arguments)
.WithValidation(CommandResultValidation.None) .WithValidation(CommandResultValidation.None)
@@ -360,7 +363,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
"Detected {Count} VAAPI profile entrypoints for using QSV device {Device}", "Detected {Count} VAAPI profile entrypoints for using QSV device {Device}",
profileEntrypoints.Count, profileEntrypoints.Count,
device); device);
_memoryCache.Set(cacheKey, profileEntrypoints); _memoryCache.Set(cacheKey, profileEntrypoints);
return new VaapiHardwareCapabilities(profileEntrypoints, _logger); return new VaapiHardwareCapabilities(profileEntrypoints, _logger);
} }
@@ -4,6 +4,7 @@ public class DecoderImplicitCuda : DecoderBase
{ {
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware;
public override string Name => string.Empty; public override string Name => string.Empty;
public override string[] InputOptions(InputFile inputFile) => public override string[] InputOptions(InputFile inputFile) =>
new[] new[]
{ {
+1 -1
View File
@@ -4,8 +4,8 @@ namespace ErsatzTV.FFmpeg.Filter;
public class CropFilter : BaseFilter public class CropFilter : BaseFilter
{ {
private readonly FrameState _currentState;
private readonly FrameSize _croppedSize; private readonly FrameSize _croppedSize;
private readonly FrameState _currentState;
public CropFilter(FrameState currentState, FrameSize croppedSize) public CropFilter(FrameState currentState, FrameSize croppedSize)
{ {
@@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter.Cuda;
public class ScaleCudaFilter : BaseFilter public class ScaleCudaFilter : BaseFilter
{ {
private readonly Option<FrameSize> _croppedSize;
private readonly FrameState _currentState; private readonly FrameState _currentState;
private readonly bool _isAnamorphicEdgeCase; private readonly bool _isAnamorphicEdgeCase;
private readonly FrameSize _paddedSize; private readonly FrameSize _paddedSize;
private readonly Option<FrameSize> _croppedSize;
private readonly FrameSize _scaledSize; private readonly FrameSize _scaledSize;
public ScaleCudaFilter( public ScaleCudaFilter(
@@ -4,10 +4,7 @@ public class NormalizeLoudnessFilter : BaseFilter
{ {
private readonly AudioFilter _loudnessFilter; private readonly AudioFilter _loudnessFilter;
public NormalizeLoudnessFilter(AudioFilter loudnessFilter) public NormalizeLoudnessFilter(AudioFilter loudnessFilter) => _loudnessFilter = loudnessFilter;
{
_loudnessFilter = loudnessFilter;
}
public override string Filter => _loudnessFilter switch public override string Filter => _loudnessFilter switch
{ {
+1 -1
View File
@@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter;
public class ScaleFilter : BaseFilter public class ScaleFilter : BaseFilter
{ {
private readonly Option<FrameSize> _croppedSize;
private readonly FrameState _currentState; private readonly FrameState _currentState;
private readonly bool _isAnamorphicEdgeCase; private readonly bool _isAnamorphicEdgeCase;
private readonly FrameSize _paddedSize; private readonly FrameSize _paddedSize;
private readonly Option<FrameSize> _croppedSize;
private readonly FrameSize _scaledSize; private readonly FrameSize _scaledSize;
public ScaleFilter( public ScaleFilter(
@@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter.Vaapi;
public class ScaleVaapiFilter : BaseFilter public class ScaleVaapiFilter : BaseFilter
{ {
private readonly Option<FrameSize> _croppedSize;
private readonly FrameState _currentState; private readonly FrameState _currentState;
private readonly bool _isAnamorphicEdgeCase; private readonly bool _isAnamorphicEdgeCase;
private readonly FrameSize _paddedSize; private readonly FrameSize _paddedSize;
private readonly Option<FrameSize> _croppedSize;
private readonly FrameSize _scaledSize; private readonly FrameSize _scaledSize;
public ScaleVaapiFilter( public ScaleVaapiFilter(
@@ -2,7 +2,6 @@
public class VaapiSubtitlePixelFormatFilter : BaseFilter 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 string Filter => "format=vaapi|yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8";
public override FrameState NextState(FrameState currentState) => currentState;
} }
+3 -4
View File
@@ -16,9 +16,8 @@ public class VideoFilter : IPipelineStep
public FrameState NextState(FrameState currentState) => currentState; public FrameState NextState(FrameState currentState) => currentState;
private string[] Arguments() => private string[] Arguments() =>
new [] [
{ "-vf",
"-vf",
string.Join(",", _filterSteps.Map(fs => fs.Filter)) string.Join(",", _filterSteps.Map(fs => fs.Filter))
}; ];
} }
+1 -1
View File
@@ -120,7 +120,7 @@ public record VideoStream(
return result; return result;
} }
public FrameSize SquarePixelFrameSizeForCrop(FrameSize resolution) public FrameSize SquarePixelFrameSizeForCrop(FrameSize resolution)
{ {
int width = FrameSize.Width; int width = FrameSize.Width;
@@ -6,8 +6,8 @@ public class OutputFormatHls : IPipelineStep
{ {
private readonly FrameState _desiredState; private readonly FrameState _desiredState;
private readonly Option<string> _mediaFrameRate; private readonly Option<string> _mediaFrameRate;
private readonly string _playlistPath;
private readonly bool _oneSecondGop; private readonly bool _oneSecondGop;
private readonly string _playlistPath;
private readonly string _segmentTemplate; private readonly string _segmentTemplate;
public OutputFormatHls( public OutputFormatHls(
@@ -37,7 +37,7 @@ public class OutputFormatHls : IPipelineStep
int frameRate = _desiredState.FrameRate.IfNone(GetFrameRateFromMedia); int frameRate = _desiredState.FrameRate.IfNone(GetFrameRateFromMedia);
int gop = _oneSecondGop ? frameRate : frameRate * SEGMENT_SECONDS; int gop = _oneSecondGop ? frameRate : frameRate * SEGMENT_SECONDS;
return new[] return new[]
{ {
"-g", $"{gop}", "-g", $"{gop}",
@@ -475,7 +475,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
subtitle.FilterSteps.Add(subtitleHardwareUpload); subtitle.FilterSteps.Add(subtitleHardwareUpload);
// only scale if scaling or padding was used for main video stream // 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); var scaleFilter = new SubtitleScaleNppFilter(desiredState.PaddedSize);
subtitle.FilterSteps.Add(scaleFilter); subtitle.FilterSteps.Add(scaleFilter);
@@ -484,17 +485,18 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
else else
{ {
// only scale if scaling or padding was used for main video stream // 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); var scaleFilter = new ScaleImageFilter(desiredState.PaddedSize);
subtitle.FilterSteps.Add(scaleFilter); subtitle.FilterSteps.Add(scaleFilter);
} }
var subtitleHardwareUpload = new HardwareUploadCudaFilter( var subtitleHardwareUpload = new HardwareUploadCudaFilter(
currentState with { FrameDataLocation = FrameDataLocation.Software }); currentState with { FrameDataLocation = FrameDataLocation.Software });
subtitle.FilterSteps.Add(subtitleHardwareUpload); subtitle.FilterSteps.Add(subtitleHardwareUpload);
} }
var subtitlesFilter = new OverlaySubtitleCudaFilter(); var subtitlesFilter = new OverlaySubtitleCudaFilter();
subtitleOverlayFilterSteps.Add(subtitlesFilter); subtitleOverlayFilterSteps.Add(subtitlesFilter);
} }
@@ -510,7 +510,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
FrameState desiredState, FrameState desiredState,
string fontsFolder, string fontsFolder,
ICollection<IPipelineStep> pipelineSteps); ICollection<IPipelineStep> pipelineSteps);
protected static FrameState SetCrop( protected static FrameState SetCrop(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
FrameState desiredState, FrameState desiredState,
@@ -631,7 +631,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
AudioFilter filter = _audioInputFile AudioFilter filter = _audioInputFile
.Map(a => a.DesiredState.NormalizeLoudnessFilter) .Map(a => a.DesiredState.NormalizeLoudnessFilter)
.IfNone(AudioFilter.None); .IfNone(AudioFilter.None);
initialBurst = filter switch initialBurst = filter switch
{ {
AudioFilter.LoudNorm => 5, AudioFilter.LoudNorm => 5,
@@ -72,17 +72,18 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
reportsFolder, reportsFolder,
fontsFolder, fontsFolder,
_logger), _logger),
HardwareAccelerationMode.VideoToolbox when capabilities is not NoHardwareCapabilities => new VideoToolboxPipelineBuilder( HardwareAccelerationMode.VideoToolbox when capabilities is not NoHardwareCapabilities => new
ffmpegCapabilities, VideoToolboxPipelineBuilder(
capabilities, ffmpegCapabilities,
hardwareAccelerationMode, capabilities,
videoInputFile, hardwareAccelerationMode,
audioInputFile, videoInputFile,
watermarkInputFile, audioInputFile,
subtitleInputFile, watermarkInputFile,
reportsFolder, subtitleInputFile,
fontsFolder, reportsFolder,
_logger), fontsFolder,
_logger),
HardwareAccelerationMode.Amf when capabilities is not NoHardwareCapabilities => new AmfPipelineBuilder( HardwareAccelerationMode.Amf when capabilities is not NoHardwareCapabilities => new AmfPipelineBuilder(
ffmpegCapabilities, ffmpegCapabilities,
capabilities, capabilities,
@@ -87,7 +87,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
{ {
pipelineSteps.Add(new NoAutoScaleOutputOption()); pipelineSteps.Add(new NoAutoScaleOutputOption());
} }
// disable hw accel if decoder/encoder isn't supported // disable hw accel if decoder/encoder isn't supported
return ffmpegState with return ffmpegState with
{ {
@@ -4,12 +4,13 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations; namespace ErsatzTV.Infrastructure.Data.Configurations;
public class PlayoutScheduleItemFillGroupIndexConfiguration : IEntityTypeConfiguration<PlayoutScheduleItemFillGroupIndex> public class
PlayoutScheduleItemFillGroupIndexConfiguration : IEntityTypeConfiguration<PlayoutScheduleItemFillGroupIndex>
{ {
public void Configure(EntityTypeBuilder<PlayoutScheduleItemFillGroupIndex> builder) public void Configure(EntityTypeBuilder<PlayoutScheduleItemFillGroupIndex> builder)
{ {
builder.ToTable("PlayoutScheduleItemFillGroupIndex"); builder.ToTable("PlayoutScheduleItemFillGroupIndex");
builder.OwnsOne(a => a.EnumeratorState).ToTable("FillGroupEnumeratorState"); builder.OwnsOne(a => a.EnumeratorState).ToTable("FillGroupEnumeratorState");
builder.HasOne(i => i.ProgramScheduleItem) builder.HasOne(i => i.ProgramScheduleItem)
@@ -210,7 +210,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Normal; episode.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbyEpisode.Id FROM EmbyEpisode @"SELECT EmbyEpisode.Id FROM EmbyEpisode
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
@@ -256,7 +256,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal; show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbyShow.Id FROM EmbyShow @"SELECT EmbyShow.Id FROM EmbyShow
INNER JOIN MediaItem MI ON MI.Id = EmbyShow.Id INNER JOIN MediaItem MI ON MI.Id = EmbyShow.Id
@@ -214,7 +214,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Normal; episode.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode @"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
@@ -237,7 +237,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
season.State = MediaItemState.Normal; season.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinSeason.Id FROM JellyfinSeason @"SELECT JellyfinSeason.Id FROM JellyfinSeason
INNER JOIN MediaItem MI ON MI.Id = JellyfinSeason.Id INNER JOIN MediaItem MI ON MI.Id = JellyfinSeason.Id
@@ -260,7 +260,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal; show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinShow.Id FROM JellyfinShow @"SELECT JellyfinShow.Id FROM JellyfinShow
INNER JOIN MediaItem MI ON MI.Id = JellyfinShow.Id INNER JOIN MediaItem MI ON MI.Id = JellyfinShow.Id
@@ -75,7 +75,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal; show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>( Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT PlexShow.Id FROM PlexShow @"SELECT PlexShow.Id FROM PlexShow
INNER JOIN MediaItem MI ON MI.Id = PlexShow.Id INNER JOIN MediaItem MI ON MI.Id = PlexShow.Id
@@ -175,7 +175,7 @@ public class TelevisionRepository : ITelevisionRepository
public async Task<List<Season>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) public async Task<List<Season>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize)
{ {
var result = new List<Season>(); var result = new List<Season>();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<ShowMetadata> maybeShowMetadata = await dbContext.ShowMetadata Option<ShowMetadata> maybeShowMetadata = await dbContext.ShowMetadata
@@ -14,8 +14,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin;
public class JellyfinApiClient : IJellyfinApiClient public class JellyfinApiClient : IJellyfinApiClient
{ {
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILogger<JellyfinApiClient> _logger; private readonly ILogger<JellyfinApiClient> _logger;
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
@@ -10,8 +10,8 @@ public class EntityLocker : IEntityLocker
private readonly ConcurrentDictionary<Type, byte> _lockedRemoteMediaSourceTypes = new(); private readonly ConcurrentDictionary<Type, byte> _lockedRemoteMediaSourceTypes = new();
private bool _embyCollections; private bool _embyCollections;
private bool _jellyfinCollections; private bool _jellyfinCollections;
private bool _plexCollections;
private bool _plex; private bool _plex;
private bool _plexCollections;
private bool _trakt; private bool _trakt;
public event EventHandler OnLibraryChanged; public event EventHandler OnLibraryChanged;
@@ -155,7 +155,7 @@ public class EntityLocker : IEntityLocker
} }
public bool AreEmbyCollectionsLocked() => _embyCollections; public bool AreEmbyCollectionsLocked() => _embyCollections;
public bool LockJellyfinCollections() public bool LockJellyfinCollections()
{ {
if (!_jellyfinCollections) if (!_jellyfinCollections)
@@ -181,7 +181,7 @@ public class EntityLocker : IEntityLocker
} }
public bool AreJellyfinCollectionsLocked() => _jellyfinCollections; public bool AreJellyfinCollectionsLocked() => _jellyfinCollections;
public bool LockPlexCollections() public bool LockPlexCollections()
{ {
if (!_plexCollections) if (!_plexCollections)
@@ -35,7 +35,7 @@ public interface IPlexServerApi
int take, int take,
[Query] [AliasAs("X-Plex-Token")] [Query] [AliasAs("X-Plex-Token")]
string token); string token);
[Get("/library/all?type=18&X-Plex-Container-Start=0&X-Plex-Container-Size=0")] [Get("/library/all?type=18&X-Plex-Container-Start=0&X-Plex-Container-Size=0")]
[Headers("Accept: text/xml")] [Headers("Accept: text/xml")]
public Task<PlexXmlMediaContainerStatsResponse> GetCollectionCount( public Task<PlexXmlMediaContainerStatsResponse> GetCollectionCount(
@@ -52,14 +52,14 @@ public interface IPlexServerApi
int take, int take,
[Query] [AliasAs("X-Plex-Token")] [Query] [AliasAs("X-Plex-Token")]
string token); string token);
[Get("/library/collections/{key}/children?X-Plex-Container-Start=0&X-Plex-Container-Size=0")] [Get("/library/collections/{key}/children?X-Plex-Container-Start=0&X-Plex-Container-Size=0")]
[Headers("Accept: text/xml")] [Headers("Accept: text/xml")]
public Task<PlexXmlMediaContainerStatsResponse> GetCollectionItemsCount( public Task<PlexXmlMediaContainerStatsResponse> GetCollectionItemsCount(
string key, string key,
[Query] [AliasAs("X-Plex-Token")] [Query] [AliasAs("X-Plex-Token")]
string token); string token);
[Get("/library/collections/{key}/children")] [Get("/library/collections/{key}/children")]
[Headers("Accept: application/json")] [Headers("Accept: application/json")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexCollectionItemMetadataResponse>>> public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexCollectionItemMetadataResponse>>>
@@ -18,7 +18,7 @@ public class PlexCollectionItemMetadataResponse
[XmlAttribute("updatedAt")] [XmlAttribute("updatedAt")]
public long UpdatedAt { get; set; } public long UpdatedAt { get; set; }
[XmlAttribute("type")] [XmlAttribute("type")]
public string Type { get; set; } public string Type { get; set; }
} }
@@ -18,13 +18,13 @@ public class PlexCollectionMetadataResponse
[XmlAttribute("updatedAt")] [XmlAttribute("updatedAt")]
public long UpdatedAt { get; set; } public long UpdatedAt { get; set; }
[XmlAttribute("smart")] [XmlAttribute("smart")]
public string Smart { get; set; } public string Smart { get; set; }
[XmlAttribute("librarySectionId")] [XmlAttribute("librarySectionId")]
public int LibrarySectionId { get; set; } public int LibrarySectionId { get; set; }
[XmlAttribute("childCount")] [XmlAttribute("childCount")]
public string ChildCount { get; set; } public string ChildCount { get; set; }
} }
@@ -36,7 +36,7 @@ public class PlexMetadataResponse
[XmlAttribute("updatedAt")] [XmlAttribute("updatedAt")]
public long UpdatedAt { get; set; } public long UpdatedAt { get; set; }
[XmlAttribute("index")] [XmlAttribute("index")]
public int Index { get; set; } public int Index { get; set; }
+3 -3
View File
@@ -253,7 +253,7 @@ public class PlexEtag
byte[] hash = SHA1.Create().ComputeHash(ms); byte[] hash = SHA1.Create().ComputeHash(ms);
return BitConverter.ToString(hash).Replace("-", string.Empty); return BitConverter.ToString(hash).Replace("-", string.Empty);
} }
public string ForCollection(PlexCollectionMetadataResponse response) public string ForCollection(PlexCollectionMetadataResponse response)
{ {
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream(); using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
@@ -267,7 +267,7 @@ public class PlexEtag
// collection updated at // collection updated at
bw.Write(response.UpdatedAt); bw.Write(response.UpdatedAt);
// collection child count // collection child count
bw.Write(response.ChildCount ?? "0"); bw.Write(response.ChildCount ?? "0");
@@ -296,7 +296,7 @@ public class PlexEtag
Art = 21, Art = 21,
File = 30, File = 30,
ChildCount = 40, ChildCount = 40,
Smart = 41 // smart collection bool Smart = 41 // smart collection bool
} }
@@ -53,7 +53,9 @@ public class PlexServerApiClient : IPlexServerApiClient
return directory return directory
// .Filter(l => l.Hidden == 0) // .Filter(l => l.Hidden == 0)
.Filter(l => l.Type.ToLowerInvariant() is "movie" or "show") .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) .Map(Project)
.Somes() .Somes()
.ToList(); .ToList();
@@ -407,7 +409,9 @@ public class PlexServerApiClient : IPlexServerApiClient
_ => None _ => None
}; };
private Option<PlexCollection> ProjectToCollection(PlexMediaSource plexMediaSource, PlexCollectionMetadataResponse item) private Option<PlexCollection> ProjectToCollection(
PlexMediaSource plexMediaSource,
PlexCollectionMetadataResponse item)
{ {
try try
{ {
@@ -432,7 +436,7 @@ public class PlexServerApiClient : IPlexServerApiClient
return None; return None;
} }
} }
private Option<MediaItem> ProjectToCollectionMediaItem(PlexCollectionItemMetadataResponse item) private Option<MediaItem> ProjectToCollectionMediaItem(PlexCollectionItemMetadataResponse item)
{ {
try try
@@ -78,7 +78,8 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
_random = new CloneableRandom(state.Seed); _random = new CloneableRandom(state.Seed);
_shuffled = Shuffle(_random); _shuffled = Shuffle(_random);
_lazyMinimumDuration = _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 }; State = new CollectionEnumeratorState { Seed = state.Seed };
while (State.Index < state.Index) while (State.Index < state.Index)
@@ -741,7 +741,7 @@ public class ElasticSearchIndex : ISearchIndex
return result; return result;
} }
private async Task<List<string>> GetLanguages(ISearchRepository searchRepository, List<string> mediaCodes) private async Task<List<string>> GetLanguages(ISearchRepository searchRepository, List<string> mediaCodes)
{ {
var englishNames = new System.Collections.Generic.HashSet<string>(); var englishNames = new System.Collections.Generic.HashSet<string>();
@@ -757,7 +757,7 @@ public class ElasticSearchIndex : ISearchIndex
return englishNames.ToList(); return englishNames.ToList();
} }
private static List<string> GetLanguageTags(IEnumerable<MediaVersion> mediaVersions) => private static List<string> GetLanguageTags(IEnumerable<MediaVersion> mediaVersions) =>
mediaVersions mediaVersions
.Map(mv => mv.Streams.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio).Map(ms => ms.Language)) .Map(mv => mv.Streams.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio).Map(ms => ms.Language))
@@ -518,7 +518,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
{ {
doc.Add(new TextField(LanguageTagField, code, Field.Store.NO)); doc.Add(new TextField(LanguageTagField, code, Field.Store.NO));
} }
var englishNames = new System.Collections.Generic.HashSet<string>(); var englishNames = new System.Collections.Generic.HashSet<string>();
foreach (string code in await searchRepository.GetAllLanguageCodes(mediaCodes)) foreach (string code in await searchRepository.GetAllLanguageCodes(mediaCodes))
{ {
@@ -809,7 +809,10 @@ public sealed class LuceneSearchIndex : ISearchIndex
new StringField(IdField, musicVideo.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), new StringField(IdField, musicVideo.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES),
new StringField(TypeField, MusicVideoType, Field.Store.YES), new StringField(TypeField, MusicVideoType, Field.Store.YES),
new TextField(TitleField, metadata.Title ?? string.Empty, Field.Store.NO), 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 TextField(LibraryNameField, musicVideo.LibraryPath.Library.Name, Field.Store.NO),
new StringField( new StringField(
LibraryIdField, LibraryIdField,
@@ -27,13 +27,13 @@ public class ElasticSearchItem : MinimalElasticSearchItem
[JsonPropertyName(LuceneSearchIndex.LanguageField)] [JsonPropertyName(LuceneSearchIndex.LanguageField)]
public List<string> Language { get; set; } public List<string> Language { get; set; }
[JsonPropertyName(LuceneSearchIndex.LanguageTagField)] [JsonPropertyName(LuceneSearchIndex.LanguageTagField)]
public List<string> LanguageTag { get; set; } public List<string> LanguageTag { get; set; }
[JsonPropertyName(LuceneSearchIndex.MinutesField)] [JsonPropertyName(LuceneSearchIndex.MinutesField)]
public int Minutes { get; set; } public int Minutes { get; set; }
[JsonPropertyName(LuceneSearchIndex.SecondsField)] [JsonPropertyName(LuceneSearchIndex.SecondsField)]
public int Seconds { get; set; } public int Seconds { get; set; }
@@ -2,4 +2,5 @@
namespace ErsatzTV.Scanner.Application.Jellyfin; 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>>;
@@ -9,10 +9,10 @@ namespace ErsatzTV.Scanner.Application.Jellyfin;
public class public class
SynchronizeJellyfinCollectionsHandler : IRequestHandler<SynchronizeJellyfinCollections, Either<BaseError, Unit>> SynchronizeJellyfinCollectionsHandler : IRequestHandler<SynchronizeJellyfinCollections, Either<BaseError, Unit>>
{ {
private readonly IConfigElementRepository _configElementRepository;
private readonly IJellyfinSecretStore _jellyfinSecretStore; private readonly IJellyfinSecretStore _jellyfinSecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IJellyfinCollectionScanner _scanner; private readonly IJellyfinCollectionScanner _scanner;
private readonly IConfigElementRepository _configElementRepository;
public SynchronizeJellyfinCollectionsHandler( public SynchronizeJellyfinCollectionsHandler(
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
@@ -42,7 +42,7 @@ public class
Task<Validation<BaseError, ConnectionParameters>> mediaSource = MediaSourceMustExist(request) Task<Validation<BaseError, ConnectionParameters>> mediaSource = MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey); .BindT(MediaSourceMustHaveApiKey);
return (await mediaSource, await ValidateLibraryRefreshInterval()) return (await mediaSource, await ValidateLibraryRefreshInterval())
.Apply( .Apply(
(connectionParameters, libraryRefreshInterval) => new RequestParameters( (connectionParameters, libraryRefreshInterval) => new RequestParameters(
@@ -51,7 +51,7 @@ public class
request.ForceScan, request.ForceScan,
libraryRefreshInterval)); libraryRefreshInterval));
} }
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() => private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval) _configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri is >= 0 and < 1_000_000) .FilterT(lri => lri is >= 0 and < 1_000_000)
@@ -9,8 +9,8 @@ namespace ErsatzTV.Scanner.Application.Plex;
public class SynchronizePlexCollectionsHandler : IRequestHandler<SynchronizePlexCollections, Either<BaseError, Unit>> public class SynchronizePlexCollectionsHandler : IRequestHandler<SynchronizePlexCollections, Either<BaseError, Unit>>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IPlexSecretStore _plexSecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlexSecretStore _plexSecretStore;
private readonly IPlexCollectionScanner _scanner; private readonly IPlexCollectionScanner _scanner;
public SynchronizePlexCollectionsHandler( public SynchronizePlexCollectionsHandler(
@@ -41,7 +41,7 @@ public class ShowNfoReader : NfoReader<ShowNfo>, IShowNfoReader
var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment }; var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment };
using var reader = XmlReader.Create(input, settings); using var reader = XmlReader.Create(input, settings);
var done = false; var done = false;
int showDepth = 0; var showDepth = 0;
while (!done && await reader.ReadAsync()) while (!done && await reader.ReadAsync())
{ {
@@ -131,7 +131,8 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
.HeadOrNone(); .HeadOrNone();
// skip folder if etag matches // 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) etag)
{ {
continue; continue;
@@ -126,7 +126,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
.HeadOrNone(); .HeadOrNone();
// skip folder if etag matches // 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) etag)
{ {
continue; continue;
@@ -10,10 +10,10 @@ namespace ErsatzTV.Scanner.Core.Plex;
public class PlexCollectionScanner : IPlexCollectionScanner public class PlexCollectionScanner : IPlexCollectionScanner
{ {
private readonly IPlexServerApiClient _plexServerApiClient;
private readonly IPlexCollectionRepository _plexCollectionRepository;
private readonly ILogger<PlexCollectionScanner> _logger; private readonly ILogger<PlexCollectionScanner> _logger;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly IPlexCollectionRepository _plexCollectionRepository;
private readonly IPlexServerApiClient _plexServerApiClient;
public PlexCollectionScanner( public PlexCollectionScanner(
IMediator mediator, IMediator mediator,
+3 -3
View File
@@ -73,7 +73,7 @@ public class Worker : BackgroundService
var scanPlexCollectionsCommand = new Command("scan-plex-collections", "Scan Plex collections"); var scanPlexCollectionsCommand = new Command("scan-plex-collections", "Scan Plex collections");
scanPlexCollectionsCommand.AddArgument(mediaSourceIdArgument); scanPlexCollectionsCommand.AddArgument(mediaSourceIdArgument);
scanPlexCollectionsCommand.AddOption(forceOption); scanPlexCollectionsCommand.AddOption(forceOption);
var scanEmbyCommand = new Command("scan-emby", "Scan an Emby library"); var scanEmbyCommand = new Command("scan-emby", "Scan an Emby library");
scanEmbyCommand.AddArgument(libraryIdArgument); scanEmbyCommand.AddArgument(libraryIdArgument);
scanEmbyCommand.AddOption(forceOption); scanEmbyCommand.AddOption(forceOption);
@@ -128,7 +128,7 @@ public class Worker : BackgroundService
await mediator.Send(scan, context.GetCancellationToken()); await mediator.Send(scan, context.GetCancellationToken());
} }
}); });
scanPlexCollectionsCommand.SetHandler( scanPlexCollectionsCommand.SetHandler(
async context => async context =>
{ {
@@ -202,7 +202,7 @@ public class Worker : BackgroundService
await mediator.Send(scan, context.GetCancellationToken()); await mediator.Send(scan, context.GetCancellationToken());
} }
}); });
scanJellyfinCollectionsCommand.SetHandler( scanJellyfinCollectionsCommand.SetHandler(
async context => async context =>
{ {
+1 -1
View File
@@ -196,7 +196,7 @@
{ {
_resolutions = restoredResolutions; _resolutions = restoredResolutions;
} }
if (!ApplicationState.TryTakeFromJson("_hardwareAccelerationKinds", out List<HardwareAccelerationKind> restoredHardwareAccelerationKinds)) if (!ApplicationState.TryTakeFromJson("_hardwareAccelerationKinds", out List<HardwareAccelerationKind> restoredHardwareAccelerationKinds))
{ {
_hardwareAccelerationKinds = await _mediator.Send(new GetSupportedHardwareAccelerationKinds(), _cts.Token); _hardwareAccelerationKinds = await _mediator.Send(new GetSupportedHardwareAccelerationKinds(), _cts.Token);
+3 -3
View File
@@ -138,7 +138,7 @@ public class ScannerService : BackgroundService
entityLocker.UnlockLibrary(request.LibraryId); entityLocker.UnlockLibrary(request.LibraryId);
} }
} }
private async Task SynchronizeLibraries(SynchronizePlexLibraries request, CancellationToken cancellationToken) private async Task SynchronizeLibraries(SynchronizePlexLibraries request, CancellationToken cancellationToken)
{ {
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
@@ -188,7 +188,7 @@ public class ScannerService : BackgroundService
entityLocker.UnlockLibrary(request.PlexLibraryId); entityLocker.UnlockLibrary(request.PlexLibraryId);
} }
} }
private async Task SynchronizePlexCollections( private async Task SynchronizePlexCollections(
SynchronizePlexCollections request, SynchronizePlexCollections request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -285,7 +285,7 @@ public class ScannerService : BackgroundService
entityLocker.UnlockLibrary(request.JellyfinLibraryId); entityLocker.UnlockLibrary(request.JellyfinLibraryId);
} }
} }
private async Task SynchronizeJellyfinCollections( private async Task SynchronizeJellyfinCollections(
SynchronizeJellyfinCollections request, SynchronizeJellyfinCollections request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
+2 -2
View File
@@ -247,7 +247,7 @@ public class SchedulerService : BackgroundService
cancellationToken); cancellationToken);
} }
} }
foreach (int mediaSourceId in mediaSourceIds) foreach (int mediaSourceId in mediaSourceIds)
{ {
await _scannerWorkerChannel.WriteAsync( await _scannerWorkerChannel.WriteAsync(
@@ -274,7 +274,7 @@ public class SchedulerService : BackgroundService
cancellationToken); cancellationToken);
} }
} }
foreach (int mediaSourceId in mediaSourceIds) foreach (int mediaSourceId in mediaSourceIds)
{ {
await _scannerWorkerChannel.WriteAsync( await _scannerWorkerChannel.WriteAsync(
@@ -35,7 +35,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
PlayoutMode is PlayoutMode.Multiple or PlayoutMode.Duration PlayoutMode is PlayoutMode.Multiple or PlayoutMode.Duration
&& CollectionType is ProgramScheduleItemCollectionType.Collection && CollectionType is ProgramScheduleItemCollectionType.Collection
or ProgramScheduleItemCollectionType.MultiCollection or ProgramScheduleItemCollectionType.SmartCollection; or ProgramScheduleItemCollectionType.MultiCollection or ProgramScheduleItemCollectionType.SmartCollection;
public PlayoutMode PlayoutMode { get; set; } public PlayoutMode PlayoutMode { get; set; }
public ProgramScheduleItemCollectionType CollectionType public ProgramScheduleItemCollectionType CollectionType