Browse Source

override languages and subtitles on schedule items (#753)

pull/756/head
Jason Dove 4 years ago committed by GitHub
parent
commit
78383bd5fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  2. 3
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  3. 20
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  4. 5
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  5. 20
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  6. 10
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  7. 10
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  8. 10
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  9. 10
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  10. 5
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  11. 3
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  12. 19
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  13. 3
      ErsatzTV.Core/Domain/ChannelSubtitleMode.cs
  14. 4
      ErsatzTV.Core/Domain/PlayoutItem.cs
  15. 3
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  16. 20
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  17. 2
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  18. 43
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  19. 3
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  20. 17
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegStreamSelector.cs
  21. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  22. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  23. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  24. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  25. 4140
      ErsatzTV.Infrastructure/Migrations/20220422195137_Add_ProgramScheduleItemLanguageCodes.Designer.cs
  26. 75
      ErsatzTV.Infrastructure/Migrations/20220422195137_Add_ProgramScheduleItemLanguageCodes.cs
  27. 18
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  28. 2
      ErsatzTV/Pages/Artist.razor
  29. 418
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  30. 2
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  31. 2
      ErsatzTV/Pages/TelevisionSeasonList.razor
  32. 3
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

6
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs

@ -24,4 +24,8 @@ public record AddProgramScheduleItem(
int? PostRollFillerId, int? PostRollFillerId,
int? TailFillerId, int? TailFillerId,
int? FallbackFillerId, int? FallbackFillerId,
int? WatermarkId) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest; int? WatermarkId,
string PreferredAudioLanguageCode,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode? SubtitleMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>,
IProgramScheduleItemRequest;

3
ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs

@ -23,4 +23,7 @@ public interface IProgramScheduleItemRequest
int? TailFillerId { get; } int? TailFillerId { get; }
int? FallbackFillerId { get; } int? FallbackFillerId { get; }
int? WatermarkId { get; } int? WatermarkId { get; }
string PreferredAudioLanguageCode { get; }
string PreferredSubtitleLanguageCode { get; }
ChannelSubtitleMode? SubtitleMode { get; }
} }

20
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -178,7 +178,10 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId, PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId, TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId, FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
}, },
PlayoutMode.One => new ProgramScheduleItemOne PlayoutMode.One => new ProgramScheduleItemOne
{ {
@ -198,7 +201,10 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId, PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId, TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId, FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
}, },
PlayoutMode.Multiple => new ProgramScheduleItemMultiple PlayoutMode.Multiple => new ProgramScheduleItemMultiple
{ {
@ -219,7 +225,10 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId, PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId, TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId, FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
}, },
PlayoutMode.Duration => new ProgramScheduleItemDuration PlayoutMode.Duration => new ProgramScheduleItemDuration
{ {
@ -241,7 +250,10 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId, PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId, TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId, FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
}, },
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}") _ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
}; };

5
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs

@ -24,7 +24,10 @@ public record ReplaceProgramScheduleItem(
int? PostRollFillerId, int? PostRollFillerId,
int? TailFillerId, int? TailFillerId,
int? FallbackFillerId, int? FallbackFillerId,
int? WatermarkId) : IProgramScheduleItemRequest; int? WatermarkId,
string PreferredAudioLanguageCode,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode? SubtitleMode) : IProgramScheduleItemRequest;
public record ReplaceProgramScheduleItems public record ReplaceProgramScheduleItems
(int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest< (int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest<

20
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -60,7 +60,10 @@ internal static class Mapper
: null, : null,
duration.Watermark != null duration.Watermark != null
? Watermarks.Mapper.ProjectToViewModel(duration.Watermark) ? Watermarks.Mapper.ProjectToViewModel(duration.Watermark)
: null), : null,
duration.PreferredAudioLanguageCode,
duration.PreferredSubtitleLanguageCode,
duration.SubtitleMode),
ProgramScheduleItemFlood flood => ProgramScheduleItemFlood flood =>
new ProgramScheduleItemFloodViewModel( new ProgramScheduleItemFloodViewModel(
flood.Id, flood.Id,
@ -104,7 +107,10 @@ internal static class Mapper
: null, : null,
flood.Watermark != null flood.Watermark != null
? Watermarks.Mapper.ProjectToViewModel(flood.Watermark) ? Watermarks.Mapper.ProjectToViewModel(flood.Watermark)
: null), : null,
flood.PreferredAudioLanguageCode,
flood.PreferredSubtitleLanguageCode,
flood.SubtitleMode),
ProgramScheduleItemMultiple multiple => ProgramScheduleItemMultiple multiple =>
new ProgramScheduleItemMultipleViewModel( new ProgramScheduleItemMultipleViewModel(
multiple.Id, multiple.Id,
@ -149,7 +155,10 @@ internal static class Mapper
: null, : null,
multiple.Watermark != null multiple.Watermark != null
? Watermarks.Mapper.ProjectToViewModel(multiple.Watermark) ? Watermarks.Mapper.ProjectToViewModel(multiple.Watermark)
: null), : null,
multiple.PreferredAudioLanguageCode,
multiple.PreferredSubtitleLanguageCode,
multiple.SubtitleMode),
ProgramScheduleItemOne one => ProgramScheduleItemOne one =>
new ProgramScheduleItemOneViewModel( new ProgramScheduleItemOneViewModel(
one.Id, one.Id,
@ -193,7 +202,10 @@ internal static class Mapper
: null, : null,
one.Watermark != null one.Watermark != null
? Watermarks.Mapper.ProjectToViewModel(one.Watermark) ? Watermarks.Mapper.ProjectToViewModel(one.Watermark)
: null), : null,
one.PreferredAudioLanguageCode,
one.PreferredSubtitleLanguageCode,
one.SubtitleMode),
_ => throw new NotSupportedException( _ => throw new NotSupportedException(
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}") $"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
}; };

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -28,7 +28,10 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
FillerPresetViewModel postRollFiller, FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller, FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller, FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark) : base( WatermarkViewModel watermark,
string preferredAudioLanguageCode,
string preferredSubtitleLanguageCode,
ChannelSubtitleMode? subtitleMode) : base(
id, id,
index, index,
startType, startType,
@ -47,7 +50,10 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
postRollFiller, postRollFiller,
tailFiller, tailFiller,
fallbackFiller, fallbackFiller,
watermark) watermark,
preferredAudioLanguageCode,
preferredSubtitleLanguageCode,
subtitleMode)
{ {
PlayoutDuration = playoutDuration; PlayoutDuration = playoutDuration;
TailMode = tailMode; TailMode = tailMode;

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -26,7 +26,10 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
FillerPresetViewModel postRollFiller, FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller, FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller, FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark) : base( WatermarkViewModel watermark,
string preferredAudioLanguageCode,
string preferredSubtitleLanguageCode,
ChannelSubtitleMode? subtitleMode) : base(
id, id,
index, index,
startType, startType,
@ -45,7 +48,10 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
postRollFiller, postRollFiller,
tailFiller, tailFiller,
fallbackFiller, fallbackFiller,
watermark) watermark,
preferredAudioLanguageCode,
preferredSubtitleLanguageCode,
subtitleMode)
{ {
} }
} }

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -27,7 +27,10 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
FillerPresetViewModel postRollFiller, FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller, FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller, FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark) : base( WatermarkViewModel watermark,
string preferredAudioLanguageCode,
string preferredSubtitleLanguageCode,
ChannelSubtitleMode? subtitleMode) : base(
id, id,
index, index,
startType, startType,
@ -46,7 +49,10 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
postRollFiller, postRollFiller,
tailFiller, tailFiller,
fallbackFiller, fallbackFiller,
watermark) => watermark,
preferredAudioLanguageCode,
preferredSubtitleLanguageCode,
subtitleMode) =>
Count = count; Count = count;
public int Count { get; } public int Count { get; }

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -26,7 +26,10 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
FillerPresetViewModel postRollFiller, FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller, FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller, FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark) : base( WatermarkViewModel watermark,
string preferredAudioLanguageCode,
string preferredSubtitleLanguageCode,
ChannelSubtitleMode? subtitleMode) : base(
id, id,
index, index,
startType, startType,
@ -45,7 +48,10 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
postRollFiller, postRollFiller,
tailFiller, tailFiller,
fallbackFiller, fallbackFiller,
watermark) watermark,
preferredAudioLanguageCode,
preferredSubtitleLanguageCode,
subtitleMode)
{ {
} }
} }

5
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -25,7 +25,10 @@ public abstract record ProgramScheduleItemViewModel(
FillerPresetViewModel PostRollFiller, FillerPresetViewModel PostRollFiller,
FillerPresetViewModel TailFiller, FillerPresetViewModel TailFiller,
FillerPresetViewModel FallbackFiller, FillerPresetViewModel FallbackFiller,
WatermarkViewModel Watermark) WatermarkViewModel Watermark,
string PreferredAudioLanguageCode,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode? SubtitleMode)
{ {
public string Name => CollectionType switch public string Name => CollectionType switch
{ {

3
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -163,6 +163,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
videoPath, videoPath,
audioPath, audioPath,
subtitles, subtitles,
playoutItemWithPath.PlayoutItem.PreferredAudioLanguageCode ?? channel.PreferredAudioLanguageCode,
playoutItemWithPath.PlayoutItem.PreferredSubtitleLanguageCode ?? channel.PreferredSubtitleLanguageCode,
playoutItemWithPath.PlayoutItem.SubtitleMode ?? channel.SubtitleMode,
playoutItemWithPath.PlayoutItem.StartOffset, playoutItemWithPath.PlayoutItem.StartOffset,
playoutItemWithPath.PlayoutItem.FinishOffset, playoutItemWithPath.PlayoutItem.FinishOffset,
request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now, request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now,

19
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -473,6 +473,9 @@ public class TranscodingTests
file, file,
file, file,
subtitles, subtitles,
string.Empty,
string.Empty,
subtitleMode,
now, now,
now + TimeSpan.FromSeconds(5), now + TimeSpan.FromSeconds(5),
now, now,
@ -550,16 +553,24 @@ public class TranscodingTests
private class FakeStreamSelector : IFFmpegStreamSelector private class FakeStreamSelector : IFFmpegStreamSelector
{ {
public Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version) => public Task<MediaStream> SelectVideoStream(MediaVersion version) =>
version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask(); version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask();
public Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version) => public Task<Option<MediaStream>> SelectAudioStream(
MediaVersion version,
StreamingMode streamingMode,
string channelNumber,
string preferredAudioLanguage) =>
Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask(); Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
public Task<Option<Domain.Subtitle>> SelectSubtitleStream( public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
Channel channel,
MediaVersion version, MediaVersion version,
List<Domain.Subtitle> subtitles) => subtitles.HeadOrNone().AsTask(); List<Domain.Subtitle> subtitles,
StreamingMode streamingMode,
string channelNumber,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode) =>
subtitles.HeadOrNone().AsTask();
} }
private static string ExecutableName(string baseName) => private static string ExecutableName(string baseName) =>

3
ErsatzTV.Core/Domain/ChannelSubtitleMode.cs

@ -5,5 +5,4 @@ public enum ChannelSubtitleMode
None = 0, None = 0,
Forced = 1, Forced = 1,
Default = 2, Default = 2,
Any = 3 Any = 3}
}

4
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -22,7 +22,9 @@ public class PlayoutItem
public string ChapterTitle { get; set; } public string ChapterTitle { get; set; }
public ChannelWatermark Watermark { get; set; } public ChannelWatermark Watermark { get; set; }
public int? WatermarkId { get; set; } public int? WatermarkId { get; set; }
public string PreferredAudioLanguageCode { get; set; }
public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode? SubtitleMode { get; set; }
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();

3
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -34,4 +34,7 @@ public abstract class ProgramScheduleItem
public FillerPreset FallbackFiller { get; set; } public FillerPreset FallbackFiller { get; set; }
public ChannelWatermark Watermark { get; set; } public ChannelWatermark Watermark { get; set; }
public int? WatermarkId { get; set; } public int? WatermarkId { get; set; }
public string PreferredAudioLanguageCode { get; set; }
public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode? SubtitleMode { get; set; }
} }

20
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -41,6 +41,9 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
string videoPath, string videoPath,
string audioPath, string audioPath,
List<Subtitle> subtitles, List<Subtitle> subtitles,
string preferredAudioLanguage,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode,
DateTimeOffset start, DateTimeOffset start,
DateTimeOffset finish, DateTimeOffset finish,
DateTimeOffset now, DateTimeOffset now,
@ -55,10 +58,21 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
long ptsOffset, long ptsOffset,
Option<int> targetFramerate) Option<int> targetFramerate)
{ {
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion); MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion); Option<MediaStream> maybeAudioStream =
await _ffmpegStreamSelector.SelectAudioStream(
audioVersion,
channel.StreamingMode,
channel.Number,
preferredAudioLanguage);
Option<Subtitle> maybeSubtitle = Option<Subtitle> maybeSubtitle =
await _ffmpegStreamSelector.SelectSubtitleStream(channel, videoVersion, subtitles); await _ffmpegStreamSelector.SelectSubtitleStream(
videoVersion,
subtitles,
channel.StreamingMode,
channel.Number,
preferredSubtitleLanguage,
subtitleMode);
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings( FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
channel.StreamingMode, channel.StreamingMode,

2
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -204,7 +204,7 @@ public class FFmpegProcessService
{ {
string outputFile = _tempFilePool.GetNextTempFile(TempFileCategory.SongBackground); string outputFile = _tempFilePool.GetNextTempFile(TempFileCategory.SongBackground);
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion); MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(videoVersion);
Option<ChannelWatermark> watermarkOverride = Option<ChannelWatermark> watermarkOverride =
videoVersion is FallbackMediaVersion or CoverArtMediaVersion videoVersion is FallbackMediaVersion or CoverArtMediaVersion

43
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -21,26 +21,30 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
} }
public Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version) => public Task<MediaStream> SelectVideoStream(MediaVersion version) =>
version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask(); version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask();
public async Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version) public async Task<Option<MediaStream>> SelectAudioStream(
MediaVersion version,
StreamingMode streamingMode,
string channelNumber,
string preferredAudioLanguage)
{ {
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect && if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(channel.PreferredAudioLanguageCode)) string.IsNullOrWhiteSpace(preferredAudioLanguage))
{ {
_logger.LogDebug( _logger.LogDebug(
"Channel {Number} is HLS Direct with no preferred audio language; using all audio streams", "Channel {Number} is HLS Direct with no preferred audio language; using all audio streams",
channel.Number); channelNumber);
return None; return None;
} }
var audioStreams = version.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).ToList(); var audioStreams = version.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).ToList();
string language = (channel.PreferredAudioLanguageCode ?? string.Empty).ToLowerInvariant(); string language = (preferredAudioLanguage ?? string.Empty).ToLowerInvariant();
if (string.IsNullOrWhiteSpace(language)) if (string.IsNullOrWhiteSpace(language))
{ {
_logger.LogDebug("Channel {Number} has no preferred audio language code", channel.Number); _logger.LogDebug("Channel {Number} has no preferred audio language code", channelNumber);
Option<string> maybeDefaultLanguage = await _configElementRepository.GetValue<string>( Option<string> maybeDefaultLanguage = await _configElementRepository.GetValue<string>(
ConfigElementKey.FFmpegPreferredLanguageCode); ConfigElementKey.FFmpegPreferredLanguageCode);
maybeDefaultLanguage.Match( maybeDefaultLanguage.Match(
@ -82,17 +86,20 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
} }
public async Task<Option<Subtitle>> SelectSubtitleStream( public async Task<Option<Subtitle>> SelectSubtitleStream(
Channel channel,
MediaVersion version, MediaVersion version,
List<Subtitle> subtitles) List<Subtitle> subtitles,
StreamingMode streamingMode,
string channelNumber,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode)
{ {
if (channel.SubtitleMode == ChannelSubtitleMode.None) if (subtitleMode == ChannelSubtitleMode.None)
{ {
return None; return None;
} }
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect && if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(channel.PreferredSubtitleLanguageCode)) string.IsNullOrWhiteSpace(preferredSubtitleLanguage))
{ {
// _logger.LogDebug( // _logger.LogDebug(
// "Channel {Number} is HLS Direct with no preferred subtitle language; using all subtitle streams", // "Channel {Number} is HLS Direct with no preferred subtitle language; using all subtitle streams",
@ -100,10 +107,10 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
return None; return None;
} }
string language = (channel.PreferredSubtitleLanguageCode ?? string.Empty).ToLowerInvariant(); string language = (preferredSubtitleLanguage ?? string.Empty).ToLowerInvariant();
if (string.IsNullOrWhiteSpace(language)) if (string.IsNullOrWhiteSpace(language))
{ {
_logger.LogDebug("Channel {Number} has no preferred subtitle language code", channel.Number); _logger.LogDebug("Channel {Number} has no preferred subtitle language code", channelNumber);
} }
else else
{ {
@ -117,7 +124,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
if (subtitles.Count > 0) if (subtitles.Count > 0)
{ {
switch (channel.SubtitleMode) switch (subtitleMode)
{ {
case ChannelSubtitleMode.Forced: case ChannelSubtitleMode.Forced:
foreach (Subtitle subtitle in subtitles.OrderBy(s => s.StreamIndex).Find(s => s.Forced)) foreach (Subtitle subtitle in subtitles.OrderBy(s => s.StreamIndex).Find(s => s.Forced))
@ -145,9 +152,9 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
_logger.LogDebug( _logger.LogDebug(
"Found no subtitles for channel {ChannelNumber} with mode {Mode} matching language {Language}", "Found no subtitles for channel {ChannelNumber} with mode {Mode} matching language {Language}",
channel.Number, channelNumber,
channel.SubtitleMode, subtitleMode,
channel.PreferredSubtitleLanguageCode); preferredSubtitleLanguage);
return None; return None;
} }

3
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs

@ -18,6 +18,9 @@ public interface IFFmpegProcessService
string videoPath, string videoPath,
string audioPath, string audioPath,
List<Subtitle> subtitles, List<Subtitle> subtitles,
string preferredAudioLanguage,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode,
DateTimeOffset start, DateTimeOffset start,
DateTimeOffset finish, DateTimeOffset finish,
DateTimeOffset now, DateTimeOffset now,

17
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegStreamSelector.cs

@ -4,8 +4,19 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg;
public interface IFFmpegStreamSelector public interface IFFmpegStreamSelector
{ {
Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version); Task<MediaStream> SelectVideoStream(MediaVersion version);
Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version);
Task<Option<Subtitle>> SelectSubtitleStream(Channel channel, MediaVersion version, List<Subtitle> subtitles); Task<Option<MediaStream>> SelectAudioStream(
MediaVersion version,
StreamingMode streamingMode,
string channelNumber,
string preferredAudioLanguage);
Task<Option<Subtitle>> SelectSubtitleStream(
MediaVersion version,
List<Subtitle> subtitles,
StreamingMode streamingMode,
string channelNumber,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode);
} }

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -73,7 +73,10 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
? FillerKind.Tail ? FillerKind.Tail
: FillerKind.None, : FillerKind.None,
CustomTitle = scheduleItem.CustomTitle, CustomTitle = scheduleItem.CustomTitle,
WatermarkId = scheduleItem.WatermarkId WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
}; };
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime); durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -50,7 +50,10 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
FillerKind = scheduleItem.GuideMode == GuideMode.Filler FillerKind = scheduleItem.GuideMode == GuideMode.Filler
? FillerKind.Tail ? FillerKind.Tail
: FillerKind.None, : FillerKind.None,
WatermarkId = scheduleItem.WatermarkId WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
}; };
DateTimeOffset peekScheduleItemStart = DateTimeOffset peekScheduleItemStart =

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -60,7 +60,10 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
FillerKind = scheduleItem.GuideMode == GuideMode.Filler FillerKind = scheduleItem.GuideMode == GuideMode.Filler
? FillerKind.Tail ? FillerKind.Tail
: FillerKind.None, : FillerKind.None,
WatermarkId = scheduleItem.WatermarkId WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
}; };
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime); // LogScheduledItem(scheduleItem, mediaItem, itemStartTime);

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -41,7 +41,10 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
FillerKind = scheduleItem.GuideMode == GuideMode.Filler FillerKind = scheduleItem.GuideMode == GuideMode.Filler
? FillerKind.Tail ? FillerKind.Tail
: FillerKind.None, : FillerKind.None,
WatermarkId = scheduleItem.WatermarkId WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
}; };
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller( DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(

4140
ErsatzTV.Infrastructure/Migrations/20220422195137_Add_ProgramScheduleItemLanguageCodes.Designer.cs generated

File diff suppressed because it is too large Load Diff

75
ErsatzTV.Infrastructure/Migrations/20220422195137_Add_ProgramScheduleItemLanguageCodes.cs

@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_ProgramScheduleItemLanguageCodes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PreferredAudioLanguageCode",
table: "ProgramScheduleItem",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PreferredSubtitleLanguageCode",
table: "ProgramScheduleItem",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SubtitleMode",
table: "ProgramScheduleItem",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PreferredAudioLanguageCode",
table: "PlayoutItem",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PreferredSubtitleLanguageCode",
table: "PlayoutItem",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SubtitleMode",
table: "PlayoutItem",
type: "INTEGER",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PreferredAudioLanguageCode",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "PreferredSubtitleLanguageCode",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "SubtitleMode",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "PreferredAudioLanguageCode",
table: "PlayoutItem");
migrationBuilder.DropColumn(
name: "PreferredSubtitleLanguageCode",
table: "PlayoutItem");
migrationBuilder.DropColumn(
name: "SubtitleMode",
table: "PlayoutItem");
}
}
}

18
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -1374,9 +1374,18 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("PlayoutId") b.Property<int>("PlayoutId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("PreferredAudioLanguageCode")
.HasColumnType("TEXT");
b.Property<string>("PreferredSubtitleLanguageCode")
.HasColumnType("TEXT");
b.Property<DateTime>("Start") b.Property<DateTime>("Start")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int?>("WatermarkId") b.Property<int?>("WatermarkId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1550,6 +1559,12 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int?>("PreRollFillerId") b.Property<int?>("PreRollFillerId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("PreferredAudioLanguageCode")
.HasColumnType("TEXT");
b.Property<string>("PreferredSubtitleLanguageCode")
.HasColumnType("TEXT");
b.Property<int>("ProgramScheduleId") b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1559,6 +1574,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<TimeSpan?>("StartTime") b.Property<TimeSpan?>("StartTime")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int?>("TailFillerId") b.Property<int?>("TailFillerId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

2
ErsatzTV/Pages/Artist.razor

@ -238,7 +238,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{ {
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null), _cts.Token); await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null), _cts.Token);
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); _navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
} }
} }

418
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -5,6 +5,7 @@
@using ErsatzTV.Application.Television @using ErsatzTV.Application.Television
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using ErsatzTV.Application.Filler @using ErsatzTV.Application.Filler
@using System.Globalization
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@using ErsatzTV.Application.Artists @using ErsatzTV.Application.Artists
@implements IDisposable @implements IDisposable
@ -89,205 +90,236 @@
@if (_selectedItem is not null) @if (_selectedItem is not null)
{ {
<EditForm Model="_selectedItem"> <EditForm Model="_selectedItem">
<FluentValidator/> <FluentValidator/>
<div style="display: flex; flex-direction: row;" class="mt-6"> <div style="display: flex; flex-direction: row;" class="mt-6">
<div style="flex-grow: 1; max-width: 400px;" class="mr-6"> <div style="flex-grow: 1; max-width: 400px;" class="mr-6">
<MudCard> <MudCard>
<MudCardContent> <MudCardContent>
<MudSelect Label="Start Type" @bind-Value="_selectedItem.StartType" For="@(() => _selectedItem.StartType)"> <MudSelect Label="Start Type" @bind-Value="_selectedItem.StartType" For="@(() => _selectedItem.StartType)">
<MudSelectItem Value="StartType.Dynamic">Dynamic</MudSelectItem> <MudSelectItem Value="StartType.Dynamic">Dynamic</MudSelectItem>
@if (!_schedule.ShuffleScheduleItems) @if (!_schedule.ShuffleScheduleItems)
{ {
<MudSelectItem Value="StartType.Fixed">Fixed</MudSelectItem> <MudSelectItem Value="StartType.Fixed">Fixed</MudSelectItem>
} }
</MudSelect> </MudSelect>
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/> <MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
<MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)"> <MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>()) @foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
{ {
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem> <MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
} }
</MudSelect> </MudSelect>
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection) @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudSelect Class="mt-3"
T="MediaCollectionViewModel"
Label="Collection"
@bind-value="_selectedItem.Collection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{ {
<MudSelect Class="mt-3" <MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
T="MediaCollectionViewModel"
Label="Collection"
@bind-value="_selectedItem.Collection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
} }
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.MultiCollection) </MudSelect>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudSelect Class="mt-3"
T="MultiCollectionViewModel"
Label="Multi Collection"
@bind-value="_selectedItem.MultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{ {
<MudSelect Class="mt-3" <MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
T="MultiCollectionViewModel"
Label="Multi Collection"
@bind-value="_selectedItem.MultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
} }
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.SmartCollection) </MudSelect>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudSelect Class="mt-3"
T="SmartCollectionViewModel"
Label="Smart Collection"
@bind-value="_selectedItem.SmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{ {
<MudSelect Class="mt-3" <MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
T="SmartCollectionViewModel"
Label="Smart Collection"
@bind-value="_selectedItem.SmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
} }
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow) </MudSelect>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Show"
@bind-value="_selectedItem.MediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{ {
<MudSelect Class="mt-3" <MudSelectItem Value="@show">@show.Name</MudSelectItem>
T="NamedMediaItemViewModel"
Label="Television Show"
@bind-value="_selectedItem.MediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
</MudSelect>
} }
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason) </MudSelect>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Season"
@bind-value="_selectedItem.MediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{ {
<MudSelect Class="mt-3" <MudSelectItem Value="@season">@season.Name</MudSelectItem>
T="NamedMediaItemViewModel"
Label="Television Season"
@bind-value="_selectedItem.MediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
</MudSelect>
} }
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Artist) </MudSelect>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Label="Artist"
@bind-value="_selectedItem.MediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{ {
<MudSelect Class="mt-3" <MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
T="NamedMediaItemViewModel"
Label="Artist"
@bind-value="_selectedItem.MediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
}
</MudSelect>
} }
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)"> </MudSelect>
@switch (_selectedItem.CollectionType) }
{ <MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
case ProgramScheduleItemCollectionType.MultiCollection: @switch (_selectedItem.CollectionType)
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> {
<MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem> case ProgramScheduleItemCollectionType.MultiCollection:
break; <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
case ProgramScheduleItemCollectionType.Collection: <MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem>
case ProgramScheduleItemCollectionType.SmartCollection: break;
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem> case ProgramScheduleItemCollectionType.Collection:
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem> case ProgramScheduleItemCollectionType.SmartCollection:
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
break; <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
default: <MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem> break;
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem> default:
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
break; <MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
} <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
</MudSelect> break;
<MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)"> }
@if (!_schedule.ShuffleScheduleItems) </MudSelect>
{ <MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">
<MudSelectItem Value="PlayoutMode.Flood">Flood</MudSelectItem> @if (!_schedule.ShuffleScheduleItems)
} {
<MudSelectItem Value="PlayoutMode.One">One</MudSelectItem> <MudSelectItem Value="PlayoutMode.Flood">Flood</MudSelectItem>
<MudSelectItem Value="PlayoutMode.Multiple">Multiple</MudSelectItem> }
<MudSelectItem Value="PlayoutMode.Duration">Duration</MudSelectItem> <MudSelectItem Value="PlayoutMode.One">One</MudSelectItem>
</MudSelect> <MudSelectItem Value="PlayoutMode.Multiple">Multiple</MudSelectItem>
<MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/> <MudSelectItem Value="PlayoutMode.Duration">Duration</MudSelectItem>
<MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/> </MudSelect>
<MudSelect Class="mt-3" Label="Tail Mode" @bind-Value="@_selectedItem.TailMode" For="@(() => _selectedItem.TailMode)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"> <MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/>
<MudSelectItem Value="@TailMode.None">(none)</MudSelectItem> <MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
<MudSelectItem Value="@TailMode.Offline">Offline</MudSelectItem> <MudSelect Class="mt-3" Label="Tail Mode" @bind-Value="@_selectedItem.TailMode" For="@(() => _selectedItem.TailMode)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)">
<MudSelectItem Value="@TailMode.Filler">Filler</MudSelectItem> <MudSelectItem Value="@TailMode.None">(none)</MudSelectItem>
</MudSelect> <MudSelectItem Value="@TailMode.Offline">Offline</MudSelectItem>
<MudTextField Class="mt-3" Label="Custom Title" @bind-Value="@_selectedItem.CustomTitle" For="@(() => _selectedItem.CustomTitle)"/> <MudSelectItem Value="@TailMode.Filler">Filler</MudSelectItem>
<MudSelect Class="mt-3" Label="Guide Mode" @bind-Value="@_selectedItem.GuideMode" For="@(() => _selectedItem.GuideMode)"> </MudSelect>
<MudSelectItem Value="@GuideMode.Normal">Normal</MudSelectItem> <MudTextField Class="mt-3" Label="Custom Title" @bind-Value="@_selectedItem.CustomTitle" For="@(() => _selectedItem.CustomTitle)"/>
<MudSelectItem Value="@GuideMode.Filler">Filler</MudSelectItem> <MudSelect Class="mt-3" Label="Guide Mode" @bind-Value="@_selectedItem.GuideMode" For="@(() => _selectedItem.GuideMode)">
</MudSelect> <MudSelectItem Value="@GuideMode.Normal">Normal</MudSelectItem>
<MudSelect Class="mt-3" Label="Watermark" @bind-Value="@_selectedItem.Watermark" For="@(() => _selectedItem.Watermark)" Clearable="true"> <MudSelectItem Value="@GuideMode.Filler">Filler</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks) </MudSelect>
{ </MudCardContent>
<MudSelectItem Value="@watermark">@watermark.Name</MudSelectItem> </MudCard>
} </div>
</MudSelect> <div style="flex-grow: 1; max-width: 400px;">
</MudCardContent> <MudCard>
</MudCard> <MudCardContent>
</div> <MudSelect T="FillerPresetViewModel"
<div style="flex-grow: 1; max-width: 400px;"> Label="Pre-Roll Filler"
<MudCard> @bind-value="_selectedItem.PreRollFiller"
<MudCardContent> Clearable="true">
<MudSelect Class="mt-3" @foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.PreRoll))
T="FillerPresetViewModel" {
Label="Pre-Roll Filler" <MudSelectItem Value="@filler">@filler.Name</MudSelectItem>
@bind-value="_selectedItem.PreRollFiller" }
Clearable="true"> </MudSelect>
@foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.PreRoll)) <MudSelect Class="mt-3"
{ T="FillerPresetViewModel"
<MudSelectItem Value="@filler">@filler.Name</MudSelectItem> Label="Mid-Roll Filler"
} @bind-value="_selectedItem.MidRollFiller"
</MudSelect> Clearable="true">
<MudSelect Class="mt-3" @foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.MidRoll))
T="FillerPresetViewModel" {
Label="Mid-Roll Filler" <MudSelectItem Value="@filler">@filler.Name</MudSelectItem>
@bind-value="_selectedItem.MidRollFiller" }
Clearable="true"> </MudSelect>
@foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.MidRoll)) <MudSelect Class="mt-3"
{ T="FillerPresetViewModel"
<MudSelectItem Value="@filler">@filler.Name</MudSelectItem> Label="Post-Roll Filler"
} @bind-value="_selectedItem.PostRollFiller"
</MudSelect> Clearable="true">
<MudSelect Class="mt-3" @foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.PostRoll))
T="FillerPresetViewModel" {
Label="Post-Roll Filler" <MudSelectItem Value="@filler">@filler.Name</MudSelectItem>
@bind-value="_selectedItem.PostRollFiller" }
Clearable="true"> </MudSelect>
@foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.PostRoll)) <MudSelect Class="mt-3"
{ T="FillerPresetViewModel"
<MudSelectItem Value="@filler">@filler.Name</MudSelectItem> Label="Tail Filler"
} @bind-value="_selectedItem.TailFiller"
</MudSelect> Clearable="true">
<MudSelect Class="mt-3" @foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.Tail))
T="FillerPresetViewModel" {
Label="Tail Filler" <MudSelectItem Value="@filler">@filler.Name</MudSelectItem>
@bind-value="_selectedItem.TailFiller" }
Clearable="true"> </MudSelect>
@foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.Tail)) <MudSelect Class="mt-3"
{ T="FillerPresetViewModel"
<MudSelectItem Value="@filler">@filler.Name</MudSelectItem> Label="Fallback Filler"
} @bind-value="_selectedItem.FallbackFiller"
</MudSelect> Clearable="true">
<MudSelect Class="mt-3" @foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.Fallback))
T="FillerPresetViewModel" {
Label="Fallback Filler" <MudSelectItem Value="@filler">@filler.Name</MudSelectItem>
@bind-value="_selectedItem.FallbackFiller" }
Clearable="true"> </MudSelect>
@foreach (FillerPresetViewModel filler in _fillerPresets.Where(f => f.FillerKind == FillerKind.Fallback)) </MudCardContent>
{ </MudCard>
<MudSelectItem Value="@filler">@filler.Name</MudSelectItem> <MudCard Class="mt-4">
} <MudCardContent>
</MudSelect> <MudSelect Label="Watermark" @bind-Value="@_selectedItem.Watermark" For="@(() => _selectedItem.Watermark)" Clearable="true">
</MudCardContent> @foreach (WatermarkViewModel watermark in _watermarks)
</MudCard> {
</div> <MudSelectItem Value="@watermark">@watermark.Name</MudSelectItem>
</div> }
</MudSelect>
<MudSelect Class="mt-3"
Label="Preferred Audio Language"
@bind-Value="_selectedItem.PreferredAudioLanguageCode"
For="@(() => _selectedItem.PreferredAudioLanguageCode)"
Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (CultureInfo culture in _availableCultures)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
}
</MudSelect>
<MudSelect Class="mt-3"
Label="Preferred Subtitle Language"
@bind-Value="_selectedItem.PreferredSubtitleLanguageCode"
For="@(() => _selectedItem.PreferredSubtitleLanguageCode)"
Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (CultureInfo culture in _availableCultures)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
}
</MudSelect>
<MudSelect T="ChannelSubtitleMode?" Class="mt-3" Label="Subtitle Mode" @bind-Value="_selectedItem.SubtitleMode" For="@(() => _selectedItem.SubtitleMode)" Clearable="true">
<MudSelectItem T="ChannelSubtitleMode?" Value="@(ChannelSubtitleMode.None)">None</MudSelectItem>
<MudSelectItem T="ChannelSubtitleMode?" Value="@(ChannelSubtitleMode.Forced)">Forced</MudSelectItem>
<MudSelectItem T="ChannelSubtitleMode?" Value="@(ChannelSubtitleMode.Default)">Default</MudSelectItem>
<MudSelectItem T="ChannelSubtitleMode?" Value="@(ChannelSubtitleMode.Any)">Any</MudSelectItem>
</MudSelect>
</MudCardContent>
</MudCard>
</div>
</div>
</EditForm> </EditForm>
} }
</MudContainer> </MudContainer>
@ -307,6 +339,7 @@
private List<NamedMediaItemViewModel> _artists; private List<NamedMediaItemViewModel> _artists;
private List<FillerPresetViewModel> _fillerPresets; private List<FillerPresetViewModel> _fillerPresets;
private List<WatermarkViewModel> _watermarks; private List<WatermarkViewModel> _watermarks;
private List<CultureInfo> _availableCultures;
private ProgramScheduleItemEditViewModel _selectedItem; private ProgramScheduleItemEditViewModel _selectedItem;
@ -337,6 +370,7 @@
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_watermarks = await _mediator.Send(new GetAllWatermarks(), _cts.Token) _watermarks = await _mediator.Send(new GetAllWatermarks(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_availableCultures = await _mediator.Send(new GetAllLanguageCodes(), _cts.Token);
string name = string.Empty; string name = string.Empty;
var shuffleScheduleItems = false; var shuffleScheduleItems = false;
@ -386,7 +420,10 @@
PostRollFiller = item.PostRollFiller, PostRollFiller = item.PostRollFiller,
TailFiller = item.TailFiller, TailFiller = item.TailFiller,
FallbackFiller = item.FallbackFiller, FallbackFiller = item.FallbackFiller,
Watermark = item.Watermark Watermark = item.Watermark,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
}; };
switch (item) switch (item)
@ -461,7 +498,10 @@
item.PostRollFiller?.Id, item.PostRollFiller?.Id,
item.TailFiller?.Id, item.TailFiller?.Id,
item.FallbackFiller?.Id, item.FallbackFiller?.Id,
item.Watermark?.Id)).ToList(); item.Watermark?.Id,
item.PreferredAudioLanguageCode,
item.PreferredSubtitleLanguageCode,
item.SubtitleMode)).ToList();
Seq<BaseError> errorMessages = await _mediator.Send(new ReplaceProgramScheduleItems(Id, items), _cts.Token).Map(e => e.LeftToSeq()); Seq<BaseError> errorMessages = await _mediator.Send(new ReplaceProgramScheduleItems(Id, items), _cts.Token).Map(e => e.LeftToSeq());

2
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -219,7 +219,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{ {
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null), _cts.Token); await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null), _cts.Token);
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); _navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
} }
} }

2
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -228,7 +228,7 @@
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
{ {
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null), _cts.Token); await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null), _cts.Token);
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); _navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
} }
} }

3
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -65,6 +65,9 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
public FillerPresetViewModel TailFiller { get; set; } public FillerPresetViewModel TailFiller { get; set; }
public FillerPresetViewModel FallbackFiller { get; set; } public FillerPresetViewModel FallbackFiller { get; set; }
public WatermarkViewModel Watermark { get; set; } public WatermarkViewModel Watermark { get; set; }
public string PreferredAudioLanguageCode { get; set; }
public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode? SubtitleMode { get; set; }
public string CollectionName => CollectionType switch public string CollectionName => CollectionType switch
{ {

Loading…
Cancel
Save