fix delete old segments (#1536)
* code cleanup * ignore errors deleting old hls segments
This commit is contained in:
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Fix error loading path replacements when using MySql
|
- Fix 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>;
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-2
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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>>;
|
||||||
|
|||||||
+3
-3
@@ -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,
|
||||||
|
|||||||
@@ -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 =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user