Browse Source

fix delete old segments (#1536)

* code cleanup

* ignore errors deleting old hls segments
pull/1537/head
Jason Dove 1 year ago committed by GitHub
parent
commit
c18be5559b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 9
      ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs
  3. 2
      ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKindsHandler.cs
  4. 4
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs
  5. 3
      ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinCollections.cs
  6. 4
      ErsatzTV.Application/Libraries/Queries/GetExternalCollectionsHandler.cs
  7. 14
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  8. 31
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  9. 8
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs
  10. 4
      ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs
  11. 4
      ErsatzTV.Core/Domain/PlayoutScheduleItemFillGroupIndex.cs
  12. 2
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  13. 2
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  14. 2
      ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs
  15. 4
      ErsatzTV.Core/Iptv/ChannelPlaylist.cs
  16. 26
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  17. 2
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  18. 4
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  19. 3
      ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs
  20. 3
      ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs
  21. 3
      ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs
  22. 6
      ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs
  23. 11
      ErsatzTV.FFmpeg/Capabilities/FFmpegKnownDecoder.cs
  24. 7
      ErsatzTV.FFmpeg/Capabilities/FFmpegKnownEncoder.cs
  25. 9
      ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs
  26. 11
      ErsatzTV.FFmpeg/Capabilities/FFmpegKnownHardwareAcceleration.cs
  27. 9
      ErsatzTV.FFmpeg/Capabilities/FFmpegKnownOption.cs
  28. 13
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  29. 1
      ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderImplicitCuda.cs
  30. 2
      ErsatzTV.FFmpeg/Filter/CropFilter.cs
  31. 2
      ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs
  32. 5
      ErsatzTV.FFmpeg/Filter/NormalizeLoudnessFilter.cs
  33. 2
      ErsatzTV.FFmpeg/Filter/ScaleFilter.cs
  34. 2
      ErsatzTV.FFmpeg/Filter/Vaapi/ScaleVaapiFilter.cs
  35. 3
      ErsatzTV.FFmpeg/Filter/Vaapi/VaapiSubtitlePixelFormatFilter.cs
  36. 7
      ErsatzTV.FFmpeg/Filter/VideoFilter.cs
  37. 2
      ErsatzTV.FFmpeg/MediaStream.cs
  38. 4
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs
  39. 10
      ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs
  40. 4
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  41. 23
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs
  42. 2
      ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs
  43. 5
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutScheduleItemFillGroupIndexConfiguration.cs
  44. 4
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  45. 6
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  46. 2
      ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs
  47. 2
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  48. 2
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  49. 6
      ErsatzTV.Infrastructure/Locking/EntityLocker.cs
  50. 6
      ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs
  51. 2
      ErsatzTV.Infrastructure/Plex/Models/PlexCollectionItemMetadataResponse.cs
  52. 6
      ErsatzTV.Infrastructure/Plex/Models/PlexCollectionMetadataResponse.cs
  53. 2
      ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs
  54. 6
      ErsatzTV.Infrastructure/Plex/PlexEtag.cs
  55. 10
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  56. 3
      ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs
  57. 4
      ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs
  58. 7
      ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs
  59. 4
      ErsatzTV.Infrastructure/Search/Models/ElasticSearchItem.cs
  60. 3
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollections.cs
  61. 6
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs
  62. 2
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexCollectionsHandler.cs
  63. 2
      ErsatzTV.Scanner/Core/Metadata/Nfo/ShowNfoReader.cs
  64. 3
      ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs
  65. 3
      ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs
  66. 4
      ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs
  67. 6
      ErsatzTV.Scanner/Worker.cs
  68. 2
      ErsatzTV/Pages/FFmpegEditor.razor
  69. 6
      ErsatzTV/Services/ScannerService.cs
  70. 4
      ErsatzTV/Services/SchedulerService.cs
  71. 2
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

1
CHANGELOG.md

@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

9
ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs

@ -2,5 +2,10 @@ using ErsatzTV.Core.Iptv; @@ -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>;

2
ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKindsHandler.cs

@ -22,7 +22,7 @@ public class @@ -22,7 +22,7 @@ public class
_dbContextFactory = dbContextFactory;
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
}
public async Task<List<HardwareAccelerationKind>> Handle(
GetSupportedHardwareAccelerationKinds request,
CancellationToken cancellationToken)

4
ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs

@ -40,7 +40,9 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy @@ -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)

3
ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinCollections.cs

@ -2,5 +2,6 @@ using ErsatzTV.Core; @@ -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;

4
ErsatzTV.Application/Libraries/Queries/GetExternalCollectionsHandler.cs

@ -36,7 +36,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti @@ -36,7 +36,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
return embyMediaSourceIds.Map(id => new LibraryViewModel("Emby", 0, "Collections", 0, id, string.Empty));
}
private static async Task<IEnumerable<LibraryViewModel>> GetJellyfinExternalCollections(
TvContext dbContext,
CancellationToken cancellationToken)
@ -49,7 +49,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti @@ -49,7 +49,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
return jellyfinMediaSourceIds.Map(
id => new LibraryViewModel("Jellyfin", 0, "Collections", 0, id, string.Empty));
}
private static async Task<IEnumerable<LibraryViewModel>> GetPlexExternalCollections(
TvContext dbContext,
CancellationToken cancellationToken)

14
ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs

@ -18,15 +18,15 @@ namespace ErsatzTV.Application.Streaming; @@ -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;
@ -40,7 +40,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -40,7 +40,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
ILogger<HlsSessionWorker> sessionWorkerLogger,
IFFmpegSegmenterService ffmpegSegmenterService,
IConfigElementRepository configElementRepository,
IHostApplicationLifetime hostApplicationLifetime,
IHostApplicationLifetime hostApplicationLifetime,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_hlsPlaylistFilter = hlsPlaylistFilter;
@ -73,7 +73,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -73,7 +73,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
Option<int> targetFramerate = await _mediator.Send(
new GetChannelFramerate(request.ChannelNumber),
cancellationToken);
var worker = new HlsSessionWorker(
_serviceScopeFactory,
_client,
@ -92,7 +92,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -92,7 +92,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_ffmpegSegmenterService.SessionWorkers.TryRemove(
request.ChannelNumber,
out IHlsSessionWorker inactiveWorker);
inactiveWorker?.Dispose();
_workerChannel.TryWrite(new ReleaseMemory(false));

31
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -22,23 +22,23 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -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,
@ -223,7 +223,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -223,7 +223,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{
_timer.Dispose();
_timer = null;
_serviceScope.Dispose();
_serviceScope = null;
}
@ -278,14 +278,14 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -278,14 +278,14 @@ public class HlsSessionWorker : IHlsSessionWorker
{
Interlocked.Increment(ref _workAheadCount);
_logger.LogInformation("HLS segmenter will work ahead for channel {Channel}", _channelNumber);
HlsSessionState nextState = _state switch
{
HlsSessionState.SeekAndRealtime => HlsSessionState.SeekAndWorkAhead,
HlsSessionState.ZeroAndRealtime => HlsSessionState.ZeroAndWorkAhead,
_ => _state
};
if (nextState != _state)
{
_logger.LogDebug("HLS session state accelerating {Last} => {Next}", _state, nextState);
@ -512,7 +512,16 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -512,7 +512,16 @@ public class HlsSessionWorker : IHlsSessionWorker
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 @@ -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)
{

8
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs

@ -17,7 +17,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase @@ -17,7 +17,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
private CancellationToken _cancellationToken;
private readonly ILogger<PlayoutModeSchedulerDuration> _logger;
public PlayoutModeSchedulerDurationTests()
{
Log.Logger = new LoggerConfiguration()
@ -725,7 +725,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase @@ -725,7 +725,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
playoutItems[6].GuideFinish.HasValue.Should().BeFalse();
}
[Test]
public void Should_Not_Have_Gap_With_Post_Roll_Pad_And_Fallback_Filler()
{
@ -742,7 +742,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase @@ -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"));
@ -820,7 +820,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase @@ -820,7 +820,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddMinutes(30));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
// THIS IS THE KEY TEST - needs to be exactly 30 minutes
(playoutItems.Last().FinishOffset - playoutItems.First().StartOffset).Should().Be(TimeSpan.FromMinutes(30));

4
ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs

@ -45,7 +45,7 @@ public abstract class SchedulerTestBase @@ -45,7 +45,7 @@ public abstract class SchedulerTestBase
{ CollectionKey.ForFillerPreset(fillerPreset), enumerator2 },
{ CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 }
};
protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators(
ProgramScheduleItem scheduleItem,
IMediaCollectionEnumerator enumerator1,
@ -98,7 +98,7 @@ public abstract class SchedulerTestBase @@ -98,7 +98,7 @@ public abstract class SchedulerTestBase
TestMovie(id2, duration, new DateTime(2020, 1, 2), chapterCount)
}
};
protected static Collection CollectionOf(IDictionary<int, TimeSpan> idsAndDurations, int chapterCount = 0)
{
var mediaItems = new List<MediaItem>();

4
ErsatzTV.Core/Domain/PlayoutScheduleItemFillGroupIndex.cs

@ -10,9 +10,9 @@ public class PlayoutScheduleItemFillGroupIndex @@ -10,9 +10,9 @@ public class PlayoutScheduleItemFillGroupIndex
[NotLogged]
public Playout Playout { get; set; }
public int ProgramScheduleItemId { get; set; }
[NotLogged]
public ProgramScheduleItem ProgramScheduleItem { get; set; }

2
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -13,8 +13,10 @@ public abstract class ProgramScheduleItem @@ -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; }

2
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -329,7 +329,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -329,7 +329,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
channel.FFmpegProfile.Resolution.Width,
channel.FFmpegProfile.Resolution.Height);
}
var desiredState = new FrameState(
playbackSettings.RealtimeOutput,
fillerKind == FillerKind.Fallback,

2
ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs

@ -67,7 +67,7 @@ public interface IPlexServerApiClient @@ -67,7 +67,7 @@ public interface IPlexServerApiClient
PlexLibrary library,
PlexConnection connection,
PlexServerAuthToken token);
IAsyncEnumerable<PlexCollection> GetAllCollections(
PlexConnection connection,
PlexServerAuthToken token,

4
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -9,9 +9,9 @@ public class ChannelPlaylist @@ -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,
@ -58,7 +58,7 @@ public class ChannelPlaylist @@ -58,7 +58,7 @@ public class ChannelPlaylist
sb.AppendLine("#KODIPROP:inputstream.ffmpegdirect.open_mode=ffmpeg");
}
string logo = Optional(channel.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone()

26
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -424,11 +424,12 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -424,11 +424,12 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken);
collectionEnumerators.Add(collectionKey, enumerator);
}
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,14 +440,15 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -439,14 +440,15 @@ 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()
.FirstOrDefault(
x => x.Name.Equals("DeserializeObject", StringComparison.OrdinalIgnoreCase) && x.IsGenericMethod &&
x.GetParameters().Length == 1)?.MakeGenericMethod(scheduleItem.GetType());
foreach (CollectionWithItems fakeCollection in fakeCollections)
{
var key = new CollectionKey
@ -473,7 +475,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -473,7 +475,7 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken);
collectionEnumerators.Add(key, enumerator);
// this makes multiple (0) work - since it needs the number of items in the collection
collectionItemCount.Add(key, fakeCollection.MediaItems.Count);
}
@ -572,7 +574,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -572,7 +574,7 @@ public class PlayoutBuilder : IPlayoutBuilder
// get the schedule item out of the sorted list
ProgramScheduleItem scheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;
// replace with the fake schedule item when filling with group
if (scheduleItem.FillWithGroupMode is not FillWithGroupMode.None)
{
@ -672,7 +674,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -672,7 +674,7 @@ public class PlayoutBuilder : IPlayoutBuilder
activeSchedule,
collectionEnumerators,
saveAnchorDate);
// build fill group indices
playout.FillGroupIndices = BuildFillGroupIndices(playout, scheduleItemsFillGroupEnumerators);
@ -684,7 +686,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -684,7 +686,7 @@ public class PlayoutBuilder : IPlayoutBuilder
Dictionary<int, IScheduleItemsEnumerator> scheduleItemsFillGroupEnumerators)
{
var result = playout.FillGroupIndices.ToList();
foreach ((int programScheduleItemId, IScheduleItemsEnumerator enumerator) in scheduleItemsFillGroupEnumerators)
{
Option<PlayoutScheduleItemFillGroupIndex> maybeFgi = Optional(
@ -694,7 +696,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -694,7 +696,7 @@ public class PlayoutBuilder : IPlayoutBuilder
{
fgi.EnumeratorState = enumerator.State;
}
if (maybeFgi.IsNone)
{
var fgi = new PlayoutScheduleItemFillGroupIndex
@ -703,7 +705,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -703,7 +705,7 @@ public class PlayoutBuilder : IPlayoutBuilder
ProgramScheduleItemId = programScheduleItemId,
EnumeratorState = enumerator.State
};
result.Add(fgi);
}
}

2
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -223,7 +223,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -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");
return new List<PlayoutItem> { playoutItem };
}
// missing pad-to-nearest-minute value is invalid; use no filler
FillerPreset invalidPadFiller = allFiller
.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue == false);

4
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -11,10 +11,8 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -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,

3
ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs

@ -15,7 +15,8 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator @@ -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 };

3
ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs

@ -34,7 +34,8 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator @@ -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)

3
ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs

@ -31,7 +31,8 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator @@ -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)

6
ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs

@ -5,10 +5,10 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -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(
@ -32,8 +32,8 @@ public class FFmpegCapabilities : IFFmpegCapabilities @@ -32,8 +32,8 @@ public class FFmpegCapabilities : IFFmpegCapabilities
{
return _ffmpegEncoders.Any(
e => e.EndsWith($"_{FFmpegKnownHardwareAcceleration.Amf.Name}", StringComparison.OrdinalIgnoreCase));
}
}
Option<FFmpegKnownHardwareAcceleration> maybeAccelToCheck = hardwareAccelerationMode switch
{
HardwareAccelerationMode.Nvenc => FFmpegKnownHardwareAcceleration.Cuda,

11
ErsatzTV.FFmpeg/Capabilities/FFmpegKnownDecoder.cs

@ -2,13 +2,6 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -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 @@ -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[]
{

7
ErsatzTV.FFmpeg/Capabilities/FFmpegKnownEncoder.cs

@ -2,12 +2,9 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -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 =>

9
ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs

@ -2,14 +2,11 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -2,14 +2,11 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public record FFmpegKnownFilter
{
public string Name { get; }
public static readonly FFmpegKnownFilter ScaleNpp = new("scale_npp");
private FFmpegKnownFilter(string Name)
{
this.Name = Name;
}
private FFmpegKnownFilter(string Name) => this.Name = Name;
public static readonly FFmpegKnownFilter ScaleNpp = new("scale_npp");
public string Name { get; }
public static IList<string> AllFilters =>
new[]

11
ErsatzTV.FFmpeg/Capabilities/FFmpegKnownHardwareAcceleration.cs

@ -2,19 +2,16 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -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[]
{

9
ErsatzTV.FFmpeg/Capabilities/FFmpegKnownOption.cs

@ -6,14 +6,11 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -6,14 +6,11 @@ namespace ErsatzTV.FFmpeg.Capabilities;
[SuppressMessage("ReSharper", "StringLiteralTypo")]
public record FFmpegKnownOption
{
public string Name { get; }
public static readonly FFmpegKnownOption ReadrateInitialBurst = new("readrate_initial_burst");
private FFmpegKnownOption(string Name)
{
this.Name = Name;
}
private FFmpegKnownOption(string Name) => this.Name = Name;
public static readonly FFmpegKnownOption ReadrateInitialBurst = new("readrate_initial_burst");
public string Name { get; }
public static IList<string> AllOptions =>
new[]

13
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -18,7 +18,10 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -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}");
@ -86,7 +89,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -86,7 +89,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
{
return new NoHardwareCapabilities();
}
if (!ffmpegCapabilities.HasHardwareAcceleration(hardwareAccelerationMode))
{
_logger.LogWarning(
@ -95,7 +98,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -95,7 +98,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return new NoHardwareCapabilities();
}
return hardwareAccelerationMode switch
{
HardwareAccelerationMode.Nvenc => await GetNvidiaCapabilities(ffmpegPath, ffmpegCapabilities),
@ -135,7 +138,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -135,7 +138,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
var arguments = option.GlobalOptions.ToList();
arguments.AddRange(QsvArguments);
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithArguments(arguments)
.WithValidation(CommandResultValidation.None)
@ -360,7 +363,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -360,7 +363,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
"Detected {Count} VAAPI profile entrypoints for using QSV device {Device}",
profileEntrypoints.Count,
device);
_memoryCache.Set(cacheKey, profileEntrypoints);
return new VaapiHardwareCapabilities(profileEntrypoints, _logger);
}

1
ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderImplicitCuda.cs

@ -4,6 +4,7 @@ public class DecoderImplicitCuda : DecoderBase @@ -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[]
{

2
ErsatzTV.FFmpeg/Filter/CropFilter.cs

@ -4,8 +4,8 @@ namespace ErsatzTV.FFmpeg.Filter; @@ -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)
{

2
ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs

@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter.Cuda; @@ -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(

5
ErsatzTV.FFmpeg/Filter/NormalizeLoudnessFilter.cs

@ -4,10 +4,7 @@ public class NormalizeLoudnessFilter : BaseFilter @@ -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
{

2
ErsatzTV.FFmpeg/Filter/ScaleFilter.cs

@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter; @@ -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(

2
ErsatzTV.FFmpeg/Filter/Vaapi/ScaleVaapiFilter.cs

@ -4,10 +4,10 @@ namespace ErsatzTV.FFmpeg.Filter.Vaapi; @@ -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(

3
ErsatzTV.FFmpeg/Filter/Vaapi/VaapiSubtitlePixelFormatFilter.cs

@ -2,7 +2,6 @@ @@ -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;
}

7
ErsatzTV.FFmpeg/Filter/VideoFilter.cs

@ -16,9 +16,8 @@ public class VideoFilter : IPipelineStep @@ -16,9 +16,8 @@ public class VideoFilter : IPipelineStep
public FrameState NextState(FrameState currentState) => currentState;
private string[] Arguments() =>
new []
{
"-vf",
[
"-vf",
string.Join(",", _filterSteps.Map(fs => fs.Filter))
};
];
}

2
ErsatzTV.FFmpeg/MediaStream.cs

@ -120,7 +120,7 @@ public record VideoStream( @@ -120,7 +120,7 @@ public record VideoStream(
return result;
}
public FrameSize SquarePixelFrameSizeForCrop(FrameSize resolution)
{
int width = FrameSize.Width;

4
ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs

@ -6,8 +6,8 @@ public class OutputFormatHls : IPipelineStep @@ -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(
@ -37,7 +37,7 @@ public class OutputFormatHls : IPipelineStep @@ -37,7 +37,7 @@ public class OutputFormatHls : IPipelineStep
int frameRate = _desiredState.FrameRate.IfNone(GetFrameRateFromMedia);
int gop = _oneSecondGop ? frameRate : frameRate * SEGMENT_SECONDS;
return new[]
{
"-g", $"{gop}",

10
ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs

@ -475,7 +475,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder @@ -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,17 +485,18 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder @@ -484,17 +485,18 @@ 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);
}
var subtitleHardwareUpload = new HardwareUploadCudaFilter(
currentState with { FrameDataLocation = FrameDataLocation.Software });
subtitle.FilterSteps.Add(subtitleHardwareUpload);
}
var subtitlesFilter = new OverlaySubtitleCudaFilter();
subtitleOverlayFilterSteps.Add(subtitlesFilter);
}

4
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -510,7 +510,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -510,7 +510,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
FrameState desiredState,
string fontsFolder,
ICollection<IPipelineStep> pipelineSteps);
protected static FrameState SetCrop(
VideoInputFile videoInputFile,
FrameState desiredState,
@ -631,7 +631,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -631,7 +631,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
AudioFilter filter = _audioInputFile
.Map(a => a.DesiredState.NormalizeLoudnessFilter)
.IfNone(AudioFilter.None);
initialBurst = filter switch
{
AudioFilter.LoudNorm => 5,

23
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs

@ -72,17 +72,18 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory @@ -72,17 +72,18 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
reportsFolder,
fontsFolder,
_logger),
HardwareAccelerationMode.VideoToolbox when capabilities is not NoHardwareCapabilities => new VideoToolboxPipelineBuilder(
ffmpegCapabilities,
capabilities,
hardwareAccelerationMode,
videoInputFile,
audioInputFile,
watermarkInputFile,
subtitleInputFile,
reportsFolder,
fontsFolder,
_logger),
HardwareAccelerationMode.VideoToolbox when capabilities is not NoHardwareCapabilities => new
VideoToolboxPipelineBuilder(
ffmpegCapabilities,
capabilities,
hardwareAccelerationMode,
videoInputFile,
audioInputFile,
watermarkInputFile,
subtitleInputFile,
reportsFolder,
fontsFolder,
_logger),
HardwareAccelerationMode.Amf when capabilities is not NoHardwareCapabilities => new AmfPipelineBuilder(
ffmpegCapabilities,
capabilities,

2
ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs

@ -87,7 +87,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder @@ -87,7 +87,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
{
pipelineSteps.Add(new NoAutoScaleOutputOption());
}
// disable hw accel if decoder/encoder isn't supported
return ffmpegState with
{

5
ErsatzTV.Infrastructure/Data/Configurations/PlayoutScheduleItemFillGroupIndexConfiguration.cs

@ -4,12 +4,13 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -4,12 +4,13 @@ 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)
{
builder.ToTable("PlayoutScheduleItemFillGroupIndex");
builder.OwnsOne(a => a.EnumeratorState).ToTable("FillGroupEnumeratorState");
builder.HasOne(i => i.ProgramScheduleItem)

4
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -210,7 +210,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -210,7 +210,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbyEpisode.Id FROM EmbyEpisode
INNER JOIN MediaItem MI ON MI.Id = EmbyEpisode.Id
@ -256,7 +256,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -256,7 +256,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT EmbyShow.Id FROM EmbyShow
INNER JOIN MediaItem MI ON MI.Id = EmbyShow.Id

6
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -214,7 +214,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -214,7 +214,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinEpisode.Id FROM JellyfinEpisode
INNER JOIN MediaItem MI ON MI.Id = JellyfinEpisode.Id
@ -237,7 +237,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -237,7 +237,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
season.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinSeason.Id FROM JellyfinSeason
INNER JOIN MediaItem MI ON MI.Id = JellyfinSeason.Id
@ -260,7 +260,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -260,7 +260,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT JellyfinShow.Id FROM JellyfinShow
INNER JOIN MediaItem MI ON MI.Id = JellyfinShow.Id

2
ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs

@ -75,7 +75,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -75,7 +75,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal;
Option<int> maybeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT PlexShow.Id FROM PlexShow
INNER JOIN MediaItem MI ON MI.Id = PlexShow.Id

2
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -175,7 +175,7 @@ public class TelevisionRepository : ITelevisionRepository @@ -175,7 +175,7 @@ public class TelevisionRepository : ITelevisionRepository
public async Task<List<Season>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize)
{
var result = new List<Season>();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<ShowMetadata> maybeShowMetadata = await dbContext.ShowMetadata

2
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -14,8 +14,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin; @@ -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;

6
ErsatzTV.Infrastructure/Locking/EntityLocker.cs

@ -10,8 +10,8 @@ public class EntityLocker : IEntityLocker @@ -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;
@ -155,7 +155,7 @@ public class EntityLocker : IEntityLocker @@ -155,7 +155,7 @@ public class EntityLocker : IEntityLocker
}
public bool AreEmbyCollectionsLocked() => _embyCollections;
public bool LockJellyfinCollections()
{
if (!_jellyfinCollections)
@ -181,7 +181,7 @@ public class EntityLocker : IEntityLocker @@ -181,7 +181,7 @@ public class EntityLocker : IEntityLocker
}
public bool AreJellyfinCollectionsLocked() => _jellyfinCollections;
public bool LockPlexCollections()
{
if (!_plexCollections)

6
ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs

@ -35,7 +35,7 @@ public interface IPlexServerApi @@ -35,7 +35,7 @@ public interface IPlexServerApi
int take,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/all?type=18&X-Plex-Container-Start=0&X-Plex-Container-Size=0")]
[Headers("Accept: text/xml")]
public Task<PlexXmlMediaContainerStatsResponse> GetCollectionCount(
@ -52,14 +52,14 @@ public interface IPlexServerApi @@ -52,14 +52,14 @@ public interface IPlexServerApi
int take,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/collections/{key}/children?X-Plex-Container-Start=0&X-Plex-Container-Size=0")]
[Headers("Accept: text/xml")]
public Task<PlexXmlMediaContainerStatsResponse> GetCollectionItemsCount(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/collections/{key}/children")]
[Headers("Accept: application/json")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexCollectionItemMetadataResponse>>>

2
ErsatzTV.Infrastructure/Plex/Models/PlexCollectionItemMetadataResponse.cs

@ -18,7 +18,7 @@ public class PlexCollectionItemMetadataResponse @@ -18,7 +18,7 @@ public class PlexCollectionItemMetadataResponse
[XmlAttribute("updatedAt")]
public long UpdatedAt { get; set; }
[XmlAttribute("type")]
public string Type { get; set; }
}

6
ErsatzTV.Infrastructure/Plex/Models/PlexCollectionMetadataResponse.cs

@ -18,13 +18,13 @@ public class PlexCollectionMetadataResponse @@ -18,13 +18,13 @@ public class PlexCollectionMetadataResponse
[XmlAttribute("updatedAt")]
public long UpdatedAt { get; set; }
[XmlAttribute("smart")]
public string Smart { get; set; }
[XmlAttribute("librarySectionId")]
public int LibrarySectionId { get; set; }
[XmlAttribute("childCount")]
public string ChildCount { get; set; }
}

2
ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs

@ -36,7 +36,7 @@ public class PlexMetadataResponse @@ -36,7 +36,7 @@ public class PlexMetadataResponse
[XmlAttribute("updatedAt")]
public long UpdatedAt { get; set; }
[XmlAttribute("index")]
public int Index { get; set; }

6
ErsatzTV.Infrastructure/Plex/PlexEtag.cs

@ -253,7 +253,7 @@ public class PlexEtag @@ -253,7 +253,7 @@ public class PlexEtag
byte[] hash = SHA1.Create().ComputeHash(ms);
return BitConverter.ToString(hash).Replace("-", string.Empty);
}
public string ForCollection(PlexCollectionMetadataResponse response)
{
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
@ -267,7 +267,7 @@ public class PlexEtag @@ -267,7 +267,7 @@ public class PlexEtag
// collection updated at
bw.Write(response.UpdatedAt);
// collection child count
bw.Write(response.ChildCount ?? "0");
@ -296,7 +296,7 @@ public class PlexEtag @@ -296,7 +296,7 @@ public class PlexEtag
Art = 21,
File = 30,
ChildCount = 40,
Smart = 41 // smart collection bool
}

10
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -53,7 +53,9 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -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 @@ -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
{
@ -432,7 +436,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -432,7 +436,7 @@ public class PlexServerApiClient : IPlexServerApiClient
return None;
}
}
private Option<MediaItem> ProjectToCollectionMediaItem(PlexCollectionItemMetadataResponse item)
{
try

3
ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs

@ -78,7 +78,8 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato @@ -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)

4
ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs

@ -741,7 +741,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -741,7 +741,7 @@ public class ElasticSearchIndex : ISearchIndex
return result;
}
private async Task<List<string>> GetLanguages(ISearchRepository searchRepository, List<string> mediaCodes)
{
var englishNames = new System.Collections.Generic.HashSet<string>();
@ -757,7 +757,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -757,7 +757,7 @@ public class ElasticSearchIndex : ISearchIndex
return englishNames.ToList();
}
private static List<string> GetLanguageTags(IEnumerable<MediaVersion> mediaVersions) =>
mediaVersions
.Map(mv => mv.Streams.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio).Map(ms => ms.Language))

7
ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs

@ -518,7 +518,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -518,7 +518,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
{
doc.Add(new TextField(LanguageTagField, code, Field.Store.NO));
}
var englishNames = new System.Collections.Generic.HashSet<string>();
foreach (string code in await searchRepository.GetAllLanguageCodes(mediaCodes))
{
@ -809,7 +809,10 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -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,

4
ErsatzTV.Infrastructure/Search/Models/ElasticSearchItem.cs

@ -27,13 +27,13 @@ public class ElasticSearchItem : MinimalElasticSearchItem @@ -27,13 +27,13 @@ public class ElasticSearchItem : MinimalElasticSearchItem
[JsonPropertyName(LuceneSearchIndex.LanguageField)]
public List<string> Language { get; set; }
[JsonPropertyName(LuceneSearchIndex.LanguageTagField)]
public List<string> LanguageTag { get; set; }
[JsonPropertyName(LuceneSearchIndex.MinutesField)]
public int Minutes { get; set; }
[JsonPropertyName(LuceneSearchIndex.SecondsField)]
public int Seconds { get; set; }

3
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollections.cs

@ -2,4 +2,5 @@ @@ -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>>;

6
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs

@ -9,10 +9,10 @@ namespace ErsatzTV.Scanner.Application.Jellyfin; @@ -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,
@ -42,7 +42,7 @@ public class @@ -42,7 +42,7 @@ public class
Task<Validation<BaseError, ConnectionParameters>> mediaSource = MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
return (await mediaSource, await ValidateLibraryRefreshInterval())
.Apply(
(connectionParameters, libraryRefreshInterval) => new RequestParameters(
@ -51,7 +51,7 @@ public class @@ -51,7 +51,7 @@ public class
request.ForceScan,
libraryRefreshInterval));
}
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri is >= 0 and < 1_000_000)

2
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexCollectionsHandler.cs

@ -9,8 +9,8 @@ namespace ErsatzTV.Scanner.Application.Plex; @@ -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(

2
ErsatzTV.Scanner/Core/Metadata/Nfo/ShowNfoReader.cs

@ -41,7 +41,7 @@ public class ShowNfoReader : NfoReader<ShowNfo>, IShowNfoReader @@ -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())
{

3
ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs

@ -131,7 +131,8 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -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;

3
ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs

@ -126,7 +126,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -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;

4
ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs

@ -10,10 +10,10 @@ namespace ErsatzTV.Scanner.Core.Plex; @@ -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,

6
ErsatzTV.Scanner/Worker.cs

@ -73,7 +73,7 @@ public class Worker : BackgroundService @@ -73,7 +73,7 @@ public class Worker : BackgroundService
var scanPlexCollectionsCommand = new Command("scan-plex-collections", "Scan Plex collections");
scanPlexCollectionsCommand.AddArgument(mediaSourceIdArgument);
scanPlexCollectionsCommand.AddOption(forceOption);
var scanEmbyCommand = new Command("scan-emby", "Scan an Emby library");
scanEmbyCommand.AddArgument(libraryIdArgument);
scanEmbyCommand.AddOption(forceOption);
@ -128,7 +128,7 @@ public class Worker : BackgroundService @@ -128,7 +128,7 @@ public class Worker : BackgroundService
await mediator.Send(scan, context.GetCancellationToken());
}
});
scanPlexCollectionsCommand.SetHandler(
async context =>
{
@ -202,7 +202,7 @@ public class Worker : BackgroundService @@ -202,7 +202,7 @@ public class Worker : BackgroundService
await mediator.Send(scan, context.GetCancellationToken());
}
});
scanJellyfinCollectionsCommand.SetHandler(
async context =>
{

2
ErsatzTV/Pages/FFmpegEditor.razor

@ -196,7 +196,7 @@ @@ -196,7 +196,7 @@
{
_resolutions = restoredResolutions;
}
if (!ApplicationState.TryTakeFromJson("_hardwareAccelerationKinds", out List<HardwareAccelerationKind> restoredHardwareAccelerationKinds))
{
_hardwareAccelerationKinds = await _mediator.Send(new GetSupportedHardwareAccelerationKinds(), _cts.Token);

6
ErsatzTV/Services/ScannerService.cs

@ -138,7 +138,7 @@ public class ScannerService : BackgroundService @@ -138,7 +138,7 @@ public class ScannerService : BackgroundService
entityLocker.UnlockLibrary(request.LibraryId);
}
}
private async Task SynchronizeLibraries(SynchronizePlexLibraries request, CancellationToken cancellationToken)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
@ -188,7 +188,7 @@ public class ScannerService : BackgroundService @@ -188,7 +188,7 @@ public class ScannerService : BackgroundService
entityLocker.UnlockLibrary(request.PlexLibraryId);
}
}
private async Task SynchronizePlexCollections(
SynchronizePlexCollections request,
CancellationToken cancellationToken)
@ -285,7 +285,7 @@ public class ScannerService : BackgroundService @@ -285,7 +285,7 @@ public class ScannerService : BackgroundService
entityLocker.UnlockLibrary(request.JellyfinLibraryId);
}
}
private async Task SynchronizeJellyfinCollections(
SynchronizeJellyfinCollections request,
CancellationToken cancellationToken)

4
ErsatzTV/Services/SchedulerService.cs

@ -247,7 +247,7 @@ public class SchedulerService : BackgroundService @@ -247,7 +247,7 @@ public class SchedulerService : BackgroundService
cancellationToken);
}
}
foreach (int mediaSourceId in mediaSourceIds)
{
await _scannerWorkerChannel.WriteAsync(
@ -274,7 +274,7 @@ public class SchedulerService : BackgroundService @@ -274,7 +274,7 @@ public class SchedulerService : BackgroundService
cancellationToken);
}
}
foreach (int mediaSourceId in mediaSourceIds)
{
await _scannerWorkerChannel.WriteAsync(

2
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -35,7 +35,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -35,7 +35,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
PlayoutMode is PlayoutMode.Multiple or PlayoutMode.Duration
&& CollectionType is ProgramScheduleItemCollectionType.Collection
or ProgramScheduleItemCollectionType.MultiCollection or ProgramScheduleItemCollectionType.SmartCollection;
public PlayoutMode PlayoutMode { get; set; }
public ProgramScheduleItemCollectionType CollectionType

Loading…
Cancel
Save