Browse Source

generate music video credits (#832)

pull/833/head
Jason Dove 3 years ago committed by GitHub
parent
commit
7644d628e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Channels/ChannelViewModel.cs
  3. 3
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  4. 18
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  5. 3
      ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
  6. 17
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  7. 3
      ErsatzTV.Application/Channels/Mapper.cs
  8. 49
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  9. 1
      ErsatzTV.Core/Domain/Channel.cs
  10. 7
      ErsatzTV.Core/Domain/ChannelMusicVideoCreditsMode.cs
  11. 78
      ErsatzTV.Core/FFmpeg/MusicVideoCreditsGenerator.cs
  12. 28
      ErsatzTV.Core/FFmpeg/SubtitleBuilder.cs
  13. 8
      ErsatzTV.Core/Interfaces/FFmpeg/IMusicVideoCreditsGenerator.cs
  14. 4294
      ErsatzTV.Infrastructure/Migrations/20220602225301_Add_ChannelMusicVideoCreditsMode.Designer.cs
  15. 26
      ErsatzTV.Infrastructure/Migrations/20220602225301_Add_ChannelMusicVideoCreditsMode.cs
  16. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  17. 57
      ErsatzTV/Pages/ChannelEditor.razor
  18. 24
      ErsatzTV/Services/RunOnce/ResourceExtractorService.cs
  19. 1
      ErsatzTV/Startup.cs
  20. 7
      ErsatzTV/ViewModels/ChannelEditViewModel.cs

4
CHANGELOG.md

@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Fix Jellyfin show library paging
### Added
- Add basic music video credits subtitle generation
- This can be enabled in channel settings
## [0.6.0-beta] - 2022-06-01
### Fixed
- Additional fix for duplicate `Other Videos` entries; trash may need to be emptied one last time after upgrading

3
ErsatzTV.Application/Channels/ChannelViewModel.cs

@ -16,4 +16,5 @@ public record ChannelViewModel( @@ -16,4 +16,5 @@ public record ChannelViewModel(
int? FallbackFillerId,
int PlayoutCount,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode);
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode);

3
ErsatzTV.Application/Channels/Commands/CreateChannel.cs

@ -16,4 +16,5 @@ public record CreateChannel @@ -16,4 +16,5 @@ public record CreateChannel
int? WatermarkId,
int? FallbackFillerId,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, CreateChannelResult>>;
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode) : IRequest<Either<BaseError, CreateChannelResult>>;

18
ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs

@ -21,7 +21,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr @@ -21,7 +21,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => PersistChannel(dbContext, c));
return await validation.Apply(c => PersistChannel(dbContext, c));
}
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
@ -36,6 +36,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr @@ -36,6 +36,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
await FFmpegProfileMustExist(dbContext, request),
ValidatePreferredAudioLanguage(request),
ValidatePreferredSubtitleLanguage(request),
ValidateSubtitleAndMusicCredits(request),
await WatermarkMustExist(dbContext, request),
await FillerPresetMustExist(dbContext, request))
.Apply(
@ -45,6 +46,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr @@ -45,6 +46,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
ffmpegProfileId,
preferredAudioLanguageCode,
preferredSubtitleLanguageCode,
_,
watermarkId,
fillerPresetId) =>
{
@ -72,7 +74,8 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr @@ -72,7 +74,8 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
Artwork = artwork,
PreferredAudioLanguageCode = preferredAudioLanguageCode,
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
SubtitleMode = request.SubtitleMode
SubtitleMode = request.SubtitleMode,
MusicVideoCreditsMode = request.MusicVideoCreditsMode
};
foreach (int id in watermarkId)
@ -106,6 +109,17 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr @@ -106,6 +109,17 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred subtitle language code is invalid");
private static Validation<BaseError, string> ValidateSubtitleAndMusicCredits(CreateChannel createChannel)
{
if (createChannel.MusicVideoCreditsMode != ChannelMusicVideoCreditsMode.None &&
createChannel.SubtitleMode == ChannelSubtitleMode.None)
{
return BaseError.New("Subtitles are required for music video credits");
}
return string.Empty;
}
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
CreateChannel createChannel)

3
ErsatzTV.Application/Channels/Commands/UpdateChannel.cs

@ -17,4 +17,5 @@ public record UpdateChannel @@ -17,4 +17,5 @@ public record UpdateChannel
int? WatermarkId,
int? FallbackFillerId,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, ChannelViewModel>>;
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode) : IRequest<Either<BaseError, ChannelViewModel>>;

17
ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs

@ -44,6 +44,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr @@ -44,6 +44,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
c.SubtitleMode = update.SubtitleMode;
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
c.Artwork ??= new List<Artwork>();
if (!string.IsNullOrWhiteSpace(update.Logo))
@ -92,8 +93,9 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr @@ -92,8 +93,9 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
(await ChannelMustExist(dbContext, request), ValidateName(request),
await ValidateNumber(dbContext, request),
ValidatePreferredAudioLanguage(request))
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
ValidatePreferredAudioLanguage(request),
ValidateSubtitleAndMusicCredits(request))
.Apply((channelToUpdate, _, _, _, _) => channelToUpdate);
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
@ -135,4 +137,15 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr @@ -135,4 +137,15 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred audio language code is invalid");
private static Validation<BaseError, string> ValidateSubtitleAndMusicCredits(UpdateChannel updateChannel)
{
if (updateChannel.MusicVideoCreditsMode != ChannelMusicVideoCreditsMode.None &&
updateChannel.SubtitleMode == ChannelSubtitleMode.None)
{
return BaseError.New("Subtitles are required for music video credits");
}
return string.Empty;
}
}

3
ErsatzTV.Application/Channels/Mapper.cs

@ -20,7 +20,8 @@ internal static class Mapper @@ -20,7 +20,8 @@ internal static class Mapper
channel.FallbackFillerId,
channel.Playouts?.Count ?? 0,
channel.PreferredSubtitleLanguageCode,
channel.SubtitleMode);
channel.SubtitleMode,
channel.MusicVideoCreditsMode);
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
new(

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

@ -27,6 +27,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -27,6 +27,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMusicVideoCreditsGenerator _musicVideoCreditsGenerator;
private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly ISongVideoGenerator _songVideoGenerator;
private readonly ITelevisionRepository _televisionRepository;
@ -42,6 +43,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -42,6 +43,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository,
ISongVideoGenerator songVideoGenerator,
IMusicVideoCreditsGenerator musicVideoCreditsGenerator,
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
: base(dbContextFactory)
{
@ -54,6 +56,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -54,6 +56,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
_televisionRepository = televisionRepository;
_artistRepository = artistRepository;
_songVideoGenerator = songVideoGenerator;
_musicVideoCreditsGenerator = musicVideoCreditsGenerator;
_logger = logger;
}
@ -96,6 +99,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -96,6 +99,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).Artist)
.ThenInclude(mv => mv.ArtistMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).OtherVideoMetadata)
.ThenInclude(ovm => ovm.Subtitles)
.Include(i => i.MediaItem)
@ -155,7 +161,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -155,7 +161,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
List<Subtitle> subtitles = GetSubtitles(playoutItemWithPath);
List<Subtitle> subtitles = await GetSubtitles(playoutItemWithPath, channel);
Command process = await _ffmpegProcessService.ForPlayoutItem(
ffmpegPath,
@ -256,22 +262,22 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -256,22 +262,22 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
return BaseError.New($"Unexpected error locating playout item for channel {channel.Number}");
}
private static List<Subtitle> GetSubtitles(PlayoutItemWithPath playoutItemWithPath)
private async Task<List<Subtitle>> GetSubtitles(
PlayoutItemWithPath playoutItemWithPath,
Channel channel)
{
List<Subtitle> allSubtitles = playoutItemWithPath.PlayoutItem.MediaItem switch
{
Episode episode => Optional(episode.EpisodeMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? new List<Subtitle>())
.IfNone(new List<Subtitle>()),
Movie movie => Optional(movie.MovieMetadata).Flatten().HeadOrNone()
Episode episode => await Optional(episode.EpisodeMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? new List<Subtitle>())
.IfNone(new List<Subtitle>()),
MusicVideo musicVideo => Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone()
.IfNoneAsync(new List<Subtitle>()),
Movie movie => await Optional(movie.MovieMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? new List<Subtitle>())
.IfNone(new List<Subtitle>()),
OtherVideo otherVideo => Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone()
.IfNoneAsync(new List<Subtitle>()),
MusicVideo musicVideo => await GetMusicVideoSubtitles(musicVideo, channel),
OtherVideo otherVideo => await Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? new List<Subtitle>())
.IfNone(new List<Subtitle>()),
.IfNoneAsync(new List<Subtitle>()),
_ => new List<Subtitle>()
};
@ -309,6 +315,27 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -309,6 +315,27 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
return allSubtitles;
}
private async Task<List<Subtitle>> GetMusicVideoSubtitles(MusicVideo musicVideo, Channel channel)
{
var subtitles = new List<Subtitle>();
bool musicVideoCredits = channel.MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.GenerateSubtitles;
if (musicVideoCredits)
{
subtitles.AddRange(
await _musicVideoCreditsGenerator.GenerateCreditsSubtitle(musicVideo, channel.FFmpegProfile));
}
else
{
subtitles.AddRange(
await Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles)
.IfNoneAsync(new List<Subtitle>()));
}
return subtitles;
}
private async Task<Either<BaseError, PlayoutItemWithPath>> CheckForFallbackFiller(
TvContext dbContext,
Channel channel,

1
ErsatzTV.Core/Domain/Channel.cs

@ -25,4 +25,5 @@ public class Channel @@ -25,4 +25,5 @@ public class Channel
public string PreferredAudioLanguageCode { get; set; }
public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode SubtitleMode { get; set; }
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
}

7
ErsatzTV.Core/Domain/ChannelMusicVideoCreditsMode.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public enum ChannelMusicVideoCreditsMode
{
None = 0,
GenerateSubtitles = 1
}

78
ErsatzTV.Core/FFmpeg/MusicVideoCreditsGenerator.cs

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
using System.Text;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
namespace ErsatzTV.Core.FFmpeg;
public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
{
private readonly ITempFilePool _tempFilePool;
public MusicVideoCreditsGenerator(ITempFilePool tempFilePool) => _tempFilePool = tempFilePool;
public async Task<Option<Subtitle>> GenerateCreditsSubtitle(MusicVideo musicVideo, FFmpegProfile ffmpegProfile)
{
const int HORIZONTAL_MARGIN_PERCENT = 3;
const int VERTICAL_MARGIN_PERCENT = 5;
var fontSize = (int)Math.Round(ffmpegProfile.Resolution.Height / 20.0);
int leftMarginPercent = HORIZONTAL_MARGIN_PERCENT;
int rightMarginPercent = HORIZONTAL_MARGIN_PERCENT;
var leftMargin = (int)Math.Round(leftMarginPercent / 100.0 * ffmpegProfile.Resolution.Width);
var rightMargin = (int)Math.Round(rightMarginPercent / 100.0 * ffmpegProfile.Resolution.Width);
var verticalMargin =
(int)Math.Round(VERTICAL_MARGIN_PERCENT / 100.0 * ffmpegProfile.Resolution.Height);
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata)
{
var sb = new StringBuilder();
string artist = string.Empty;
foreach (ArtistMetadata artistMetadata in Optional(metadata.MusicVideo?.Artist?.ArtistMetadata).Flatten())
{
artist = artistMetadata.Title;
}
if (!string.IsNullOrWhiteSpace(artist))
{
sb.Append(artist);
}
if (!string.IsNullOrWhiteSpace(metadata.Title))
{
sb.Append($"\\N\"{metadata.Title}\"");
}
if (!string.IsNullOrWhiteSpace(metadata.Album))
{
sb.Append($"\\N{metadata.Album}");
}
string subtitles = await new SubtitleBuilder(_tempFilePool)
.WithResolution(ffmpegProfile.Resolution)
.WithFontName("OPTIKabel-Heavy")
.WithFontSize(fontSize)
.WithPrimaryColor("&HFFFFFF")
.WithOutlineColor("&H444444")
.WithAlignment(0)
.WithMarginRight(rightMargin)
.WithMarginLeft(leftMargin)
.WithMarginV(verticalMargin)
.WithBorderStyle(1)
.WithShadow(3)
.WithFormattedContent(sb.ToString())
.WithStartEnd(TimeSpan.FromSeconds(9), TimeSpan.FromSeconds(16))
.BuildFile();
return new Subtitle
{
Codec = "ass", Default = true, Forced = true, IsExtracted = false, SubtitleKind = SubtitleKind.Sidecar,
Path = subtitles, SDH = false
};
}
return None;
}
}

28
ErsatzTV.Core/FFmpeg/SubtitleBuilder.cs

@ -9,6 +9,7 @@ public class SubtitleBuilder @@ -9,6 +9,7 @@ public class SubtitleBuilder
private Option<int> _alignment;
private Option<int> _borderStyle;
private string _content;
private Option<TimeSpan> _end;
private Option<string> _fontName;
private Option<int> _fontSize;
private int _marginLeft;
@ -18,6 +19,7 @@ public class SubtitleBuilder @@ -18,6 +19,7 @@ public class SubtitleBuilder
private Option<string> _primaryColor;
private Option<IDisplaySize> _resolution = None;
private Option<int> _shadow;
private Option<TimeSpan> _start;
public SubtitleBuilder(ITempFilePool tempFilePool) => _tempFilePool = tempFilePool;
@ -93,6 +95,13 @@ public class SubtitleBuilder @@ -93,6 +95,13 @@ public class SubtitleBuilder
return this;
}
public SubtitleBuilder WithStartEnd(TimeSpan start, TimeSpan end)
{
_start = start;
_end = end;
return this;
}
public async Task<string> BuildFile()
{
string fileName = _tempFilePool.GetNextTempFile(TempFileCategory.Subtitle);
@ -116,16 +125,23 @@ public class SubtitleBuilder @@ -116,16 +125,23 @@ public class SubtitleBuilder
sb.AppendLine(
$"Style: Default,{await _fontName.IfNoneAsync("")},{await _fontSize.IfNoneAsync(32)},{await _primaryColor.IfNoneAsync("")},{await _outlineColor.IfNoneAsync("")},{await _borderStyle.IfNoneAsync(0)},1,{await _shadow.IfNoneAsync(0)},{await _alignment.IfNoneAsync(0)},1");
sb.AppendLine("[Events]");
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
sb.AppendLine(
$"Dialogue: 0,0:00:00.00,99:99:99.99,Default,,{_marginLeft},{_marginRight},{_marginV},,{_content}");
var start = "0:00:00.00";
foreach (TimeSpan startTime in _start)
{
start = $"{(int)startTime.TotalHours:00}:{startTime.ToString(@"mm\:ss\.ff")}";
}
if (!string.IsNullOrWhiteSpace(_content))
var end = "99:99:99.99";
foreach (TimeSpan endTime in _end)
{
sb.AppendLine(_content);
end = $"{(int)endTime.TotalHours:00}:{endTime.ToString(@"mm\:ss\.ff")}";
}
sb.AppendLine("[Events]");
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
sb.AppendLine(
@$"Dialogue: 0,{start},{end},Default,,{_marginLeft},{_marginRight},{_marginV},,{{\fad(1200,1200)}}{_content}");
await File.WriteAllTextAsync(fileName, sb.ToString());
return fileName;

8
ErsatzTV.Core/Interfaces/FFmpeg/IMusicVideoCreditsGenerator.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.FFmpeg;
public interface IMusicVideoCreditsGenerator
{
Task<Option<Subtitle>> GenerateCreditsSubtitle(MusicVideo musicVideo, FFmpegProfile ffmpegProfile);
}

4294
ErsatzTV.Infrastructure/Migrations/20220602225301_Add_ChannelMusicVideoCreditsMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

26
ErsatzTV.Infrastructure/Migrations/20220602225301_Add_ChannelMusicVideoCreditsMode.cs

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_ChannelMusicVideoCreditsMode : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MusicVideoCreditsMode",
table: "Channel",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MusicVideoCreditsMode",
table: "Channel");
}
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -233,6 +233,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -233,6 +233,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.HasColumnType("TEXT")
.HasDefaultValue("ErsatzTV");
b.Property<int>("MusicVideoCreditsMode")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");

57
ErsatzTV/Pages/ChannelEditor.razor

@ -10,10 +10,10 @@ @@ -10,10 +10,10 @@
@using ErsatzTV.Core.Domain.Filler
@using ErsatzTV.Application.Channels
@implements IDisposable
@inject NavigationManager _navigationManager
@inject ILogger<ChannelEditor> _logger
@inject ISnackbar _snackbar
@inject IMediator _mediator
@inject NavigationManager NavigationManager
@inject ILogger<ChannelEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div style="max-width: 400px;">
@ -71,6 +71,10 @@ @@ -71,6 +71,10 @@
<MudSelectItem Value="@(ChannelSubtitleMode.Default)">Default</MudSelectItem>
<MudSelectItem Value="@(ChannelSubtitleMode.Any)">Any</MudSelectItem>
</MudSelect>
<MudSelect Class="mt-3" Label="Music Video Credits Mode" @bind-Value="_model.MusicVideoCreditsMode" For="@(() => _model.MusicVideoCreditsMode)">
<MudSelectItem Value="@(ChannelMusicVideoCreditsMode.None)">None</MudSelectItem>
<MudSelectItem Value="@(ChannelMusicVideoCreditsMode.GenerateSubtitles)">Generate Subtitles</MudSelectItem>
</MudSelect>
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6">
<InputFile id="fileInput" OnChange="UploadLogo" hidden/>
@ -144,13 +148,13 @@ @@ -144,13 +148,13 @@
protected override async Task OnParametersSetAsync()
{
await LoadFFmpegProfiles(_cts.Token);
_availableCultures = await _mediator.Send(new GetAllLanguageCodes(), _cts.Token);
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), _cts.Token);
await LoadWatermarks(_cts.Token);
await LoadFillerPresets(_cts.Token);
if (Id.HasValue)
{
Option<ChannelViewModel> maybeChannel = await _mediator.Send(new GetChannelById(Id.Value), _cts.Token);
Option<ChannelViewModel> maybeChannel = await Mediator.Send(new GetChannelById(Id.Value), _cts.Token);
maybeChannel.Match(
channelViewModel =>
{
@ -167,15 +171,16 @@ @@ -167,15 +171,16 @@
_model.FallbackFillerId = channelViewModel.FallbackFillerId;
_model.PreferredSubtitleLanguageCode = channelViewModel.PreferredSubtitleLanguageCode;
_model.SubtitleMode = channelViewModel.SubtitleMode;
_model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode;
},
() => _navigationManager.NavigateTo("404"));
() => NavigationManager.NavigateTo("404"));
}
else
{
FFmpegSettingsViewModel ffmpegSettings = await _mediator.Send(new GetFFmpegSettings(), _cts.Token);
FFmpegSettingsViewModel ffmpegSettings = await Mediator.Send(new GetFFmpegSettings(), _cts.Token);
// TODO: command for new channel
IEnumerable<int> channelNumbers = await _mediator.Send(new GetAllChannels(), _cts.Token)
IEnumerable<int> channelNumbers = await Mediator.Send(new GetAllChannels(), _cts.Token)
.Map(list => list.Map(c => int.TryParse(c.Number.Split(".").Head(), out int result) ? result : 0));
int maxNumber = Optional(channelNumbers).Flatten().DefaultIfEmpty(0).Max();
_model.Number = (maxNumber + 1).ToString();
@ -195,13 +200,13 @@ @@ -195,13 +200,13 @@
private bool IsEdit => Id.HasValue;
private async Task LoadFFmpegProfiles(CancellationToken cancellationToken) =>
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles(), cancellationToken);
_ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles(), cancellationToken);
private async Task LoadWatermarks(CancellationToken cancellationToken) =>
_watermarks = await _mediator.Send(new GetAllWatermarks(), cancellationToken);
_watermarks = await Mediator.Send(new GetAllWatermarks(), cancellationToken);
private async Task LoadFillerPresets(CancellationToken cancellationToken) =>
_fillerPresets = await _mediator.Send(new GetAllFillerPresets(), cancellationToken)
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), cancellationToken)
.Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList());
private async Task HandleSubmitAsync()
@ -210,16 +215,16 @@ @@ -210,16 +215,16 @@
if (_editContext.Validate())
{
Seq<BaseError> errorMessage = IsEdit ?
(await _mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() :
(await _mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
(await Mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() :
(await Mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match(
error =>
{
_snackbar.Add(error.Value, Severity.Error);
_logger.LogError("Unexpected error saving channel: {Error}", error.Value);
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving channel: {Error}", error.Value);
},
() => _navigationManager.NavigateTo("/channels"));
() => NavigationManager.NavigateTo("/channels"));
}
}
@ -228,7 +233,7 @@ @@ -228,7 +233,7 @@
try
{
Either<BaseError, string> maybeCacheFileName =
await _mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Logo), _cts.Token);
await Mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Logo), _cts.Token);
maybeCacheFileName.Match(
relativeFileName =>
{
@ -237,21 +242,19 @@ @@ -237,21 +242,19 @@
},
error =>
{
Console.WriteLine($"error saving {error}");
_snackbar.Add($"Unexpected error saving channel logo: {error.Value}", Severity.Error);
_logger.LogError("Unexpected error saving channel logo: {Error}", error.Value);
Snackbar.Add($"Unexpected error saving channel logo: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving channel logo: {Error}", error.Value);
});
}
catch (IOException ex)
catch (IOException)
{
Console.WriteLine(ex);
_snackbar.Add("Channel logo exceeds maximum allowed file size of 10 MB", Severity.Error);
_logger.LogError("Channel logo exceeds maximum allowed file size of 10 MB");
Snackbar.Add("Channel logo exceeds maximum allowed file size of 10 MB", Severity.Error);
Logger.LogError("Channel logo exceeds maximum allowed file size of 10 MB");
}
catch (Exception ex)
{
_snackbar.Add($"Unexpected error saving channel logo: {ex.Message}", Severity.Error);
_logger.LogError("Unexpected error saving channel logo: {Error}", ex.Message);
Snackbar.Add($"Unexpected error saving channel logo: {ex.Message}", Severity.Error);
Logger.LogError("Unexpected error saving channel logo: {Error}", ex.Message);
}
}

24
ErsatzTV/Services/RunOnce/ResourceExtractorService.cs

@ -19,13 +19,14 @@ public class ResourceExtractorService : IHostedService @@ -19,13 +19,14 @@ public class ResourceExtractorService : IHostedService
await ExtractResource(assembly, "song_background_2.png", cancellationToken);
await ExtractResource(assembly, "song_background_3.png", cancellationToken);
await ExtractResource(assembly, "ErsatzTV.png", cancellationToken);
await ExtractResource(assembly, "Roboto-Regular.ttf", cancellationToken);
await ExtractResource(assembly, "OPTIKabel-Heavy.otf", cancellationToken);
await ExtractFontResource(assembly, "Roboto-Regular.ttf", cancellationToken);
await ExtractFontResource(assembly, "OPTIKabel-Heavy.otf", cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private async Task ExtractResource(Assembly assembly, string name, CancellationToken cancellationToken)
private static async Task ExtractResource(Assembly assembly, string name, CancellationToken cancellationToken)
{
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
if (resource != null)
@ -35,4 +36,21 @@ public class ResourceExtractorService : IHostedService @@ -35,4 +36,21 @@ public class ResourceExtractorService : IHostedService
await resource.CopyToAsync(fs, cancellationToken);
}
}
private static async Task ExtractFontResource(Assembly assembly, string name, CancellationToken cancellationToken)
{
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
if (resource != null)
{
await using FileStream fs = File.Create(
Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
await resource.CopyToAsync(fs, cancellationToken);
resource.Position = 0;
await using FileStream fontCacheFileStream = File.Create(
Path.Combine(FileSystemLayout.FontsCacheFolder, name));
await resource.CopyToAsync(fontCacheFileStream, cancellationToken);
}
}
}

1
ErsatzTV/Startup.cs

@ -401,6 +401,7 @@ public class Startup @@ -401,6 +401,7 @@ public class Startup
services.AddScoped<FFmpegProcessService>();
services.AddScoped<ISongVideoGenerator, SongVideoGenerator>();
services.AddScoped<IMusicVideoCreditsGenerator, MusicVideoCreditsGenerator>();
services.AddScoped<HlsSessionWorker>();
services.AddScoped<IGitHubApiClient, GitHubApiClient>();
services.AddScoped<IHtmlSanitizer, HtmlSanitizer>(

7
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -18,6 +18,7 @@ public class ChannelEditViewModel @@ -18,6 +18,7 @@ public class ChannelEditViewModel
public int? FallbackFillerId { get; set; }
public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode SubtitleMode { get; set; }
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
public UpdateChannel ToUpdate() =>
new(
@ -33,7 +34,8 @@ public class ChannelEditViewModel @@ -33,7 +34,8 @@ public class ChannelEditViewModel
WatermarkId,
FallbackFillerId,
PreferredSubtitleLanguageCode,
SubtitleMode);
SubtitleMode,
MusicVideoCreditsMode);
public CreateChannel ToCreate() =>
new(
@ -48,5 +50,6 @@ public class ChannelEditViewModel @@ -48,5 +50,6 @@ public class ChannelEditViewModel
WatermarkId,
FallbackFillerId,
PreferredSubtitleLanguageCode,
SubtitleMode);
SubtitleMode,
MusicVideoCreditsMode);
}

Loading…
Cancel
Save