Browse Source

override languages and subtitles on schedule items (#753)

pull/756/head
Jason Dove 3 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( @@ -24,4 +24,8 @@ public record AddProgramScheduleItem(
int? PostRollFillerId,
int? TailFillerId,
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 @@ -23,4 +23,7 @@ public interface IProgramScheduleItemRequest
int? TailFillerId { get; }
int? FallbackFillerId { 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 @@ -178,7 +178,10 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
},
PlayoutMode.One => new ProgramScheduleItemOne
{
@ -198,7 +201,10 @@ public abstract class ProgramScheduleItemCommandBase @@ -198,7 +201,10 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
},
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
{
@ -219,7 +225,10 @@ public abstract class ProgramScheduleItemCommandBase @@ -219,7 +225,10 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
},
PlayoutMode.Duration => new ProgramScheduleItemDuration
{
@ -241,7 +250,10 @@ public abstract class ProgramScheduleItemCommandBase @@ -241,7 +250,10 @@ public abstract class ProgramScheduleItemCommandBase
PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId,
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}")
};

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

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

20
ErsatzTV.Application/ProgramSchedules/Mapper.cs

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

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

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

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

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

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

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

10
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

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

5
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

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

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

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

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

@ -473,6 +473,9 @@ public class TranscodingTests @@ -473,6 +473,9 @@ public class TranscodingTests
file,
file,
subtitles,
string.Empty,
string.Empty,
subtitleMode,
now,
now + TimeSpan.FromSeconds(5),
now,
@ -550,16 +553,24 @@ public class TranscodingTests @@ -550,16 +553,24 @@ public class TranscodingTests
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();
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();
public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
Channel channel,
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) =>

3
ErsatzTV.Core/Domain/ChannelSubtitleMode.cs

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

4
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -22,7 +22,9 @@ public class PlayoutItem @@ -22,7 +22,9 @@ public class PlayoutItem
public string ChapterTitle { get; set; }
public ChannelWatermark Watermark { 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 FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();

3
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -34,4 +34,7 @@ public abstract class ProgramScheduleItem @@ -34,4 +34,7 @@ public abstract class ProgramScheduleItem
public FillerPreset FallbackFiller { get; set; }
public ChannelWatermark Watermark { 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 @@ -41,6 +41,9 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
string videoPath,
string audioPath,
List<Subtitle> subtitles,
string preferredAudioLanguage,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode,
DateTimeOffset start,
DateTimeOffset finish,
DateTimeOffset now,
@ -55,10 +58,21 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -55,10 +58,21 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
long ptsOffset,
Option<int> targetFramerate)
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(videoVersion);
Option<MediaStream> maybeAudioStream =
await _ffmpegStreamSelector.SelectAudioStream(
audioVersion,
channel.StreamingMode,
channel.Number,
preferredAudioLanguage);
Option<Subtitle> maybeSubtitle =
await _ffmpegStreamSelector.SelectSubtitleStream(channel, videoVersion, subtitles);
await _ffmpegStreamSelector.SelectSubtitleStream(
videoVersion,
subtitles,
channel.StreamingMode,
channel.Number,
preferredSubtitleLanguage,
subtitleMode);
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
channel.StreamingMode,

2
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

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

43
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -21,26 +21,30 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -21,26 +21,30 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
_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();
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 &&
string.IsNullOrWhiteSpace(channel.PreferredAudioLanguageCode))
if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(preferredAudioLanguage))
{
_logger.LogDebug(
"Channel {Number} is HLS Direct with no preferred audio language; using all audio streams",
channel.Number);
channelNumber);
return None;
}
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))
{
_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>(
ConfigElementKey.FFmpegPreferredLanguageCode);
maybeDefaultLanguage.Match(
@ -82,17 +86,20 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -82,17 +86,20 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
}
public async Task<Option<Subtitle>> SelectSubtitleStream(
Channel channel,
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;
}
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(channel.PreferredSubtitleLanguageCode))
if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(preferredSubtitleLanguage))
{
// _logger.LogDebug(
// "Channel {Number} is HLS Direct with no preferred subtitle language; using all subtitle streams",
@ -100,10 +107,10 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -100,10 +107,10 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
return None;
}
string language = (channel.PreferredSubtitleLanguageCode ?? string.Empty).ToLowerInvariant();
string language = (preferredSubtitleLanguage ?? string.Empty).ToLowerInvariant();
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
{
@ -117,7 +124,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -117,7 +124,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
if (subtitles.Count > 0)
{
switch (channel.SubtitleMode)
switch (subtitleMode)
{
case ChannelSubtitleMode.Forced:
foreach (Subtitle subtitle in subtitles.OrderBy(s => s.StreamIndex).Find(s => s.Forced))
@ -145,9 +152,9 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -145,9 +152,9 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
_logger.LogDebug(
"Found no subtitles for channel {ChannelNumber} with mode {Mode} matching language {Language}",
channel.Number,
channel.SubtitleMode,
channel.PreferredSubtitleLanguageCode);
channelNumber,
subtitleMode,
preferredSubtitleLanguage);
return None;
}

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

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

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

@ -4,8 +4,19 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg; @@ -4,8 +4,19 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg;
public interface IFFmpegStreamSelector
{
Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version);
Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version);
Task<MediaStream> SelectVideoStream(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 @@ -73,7 +73,10 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
? FillerKind.Tail
: FillerKind.None,
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);

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

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

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

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

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -41,7 +41,10 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI @@ -41,7 +41,10 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
? FillerKind.Tail
: FillerKind.None,
WatermarkId = scheduleItem.WatermarkId
WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
};
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 @@ @@ -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 @@ -1374,9 +1374,18 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<string>("PreferredAudioLanguageCode")
.HasColumnType("TEXT");
b.Property<string>("PreferredSubtitleLanguageCode")
.HasColumnType("TEXT");
b.Property<DateTime>("Start")
.HasColumnType("TEXT");
b.Property<int?>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int?>("WatermarkId")
.HasColumnType("INTEGER");
@ -1550,6 +1559,12 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1550,6 +1559,12 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int?>("PreRollFillerId")
.HasColumnType("INTEGER");
b.Property<string>("PreferredAudioLanguageCode")
.HasColumnType("TEXT");
b.Property<string>("PreferredSubtitleLanguageCode")
.HasColumnType("TEXT");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
@ -1559,6 +1574,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1559,6 +1574,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<TimeSpan?>("StartTime")
.HasColumnType("TEXT");
b.Property<int?>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int?>("TailFillerId")
.HasColumnType("INTEGER");

2
ErsatzTV/Pages/Artist.razor

@ -238,7 +238,7 @@ @@ -238,7 +238,7 @@
DialogResult result = await dialog.Result;
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");
}
}

418
ErsatzTV/Pages/ScheduleItemsEditor.razor

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

2
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -219,7 +219,7 @@ @@ -219,7 +219,7 @@
DialogResult result = await dialog.Result;
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");
}
}

2
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -228,7 +228,7 @@ @@ -228,7 +228,7 @@
DialogResult result = await dialog.Result;
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");
}
}

3
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -65,6 +65,9 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -65,6 +65,9 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
public FillerPresetViewModel TailFiller { get; set; }
public FillerPresetViewModel FallbackFiller { 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
{

Loading…
Cancel
Save