add music video credits template system (#982)
* add music video credits template system * fix search index bug * cleanup csproj
This commit is contained in:
@@ -8,6 +8,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Fix bug where tail or fallback filler would sometimes schedule much longer than expected
|
||||
- This only happened with fixed start schedule items following a schedule item with tail or fallback filler
|
||||
|
||||
### Added
|
||||
- Add music video credits template system
|
||||
- Templates are selected in each channel's settings
|
||||
- Templates should be copied from `_default.ass.sbntxt` which is located in the config subfolder `templates/music-video-credits`
|
||||
- Copy the file, give it any name ending with `.ass.sbntext`, and only make edits to the copied file
|
||||
- The default template will be extracted and overwritten every time ErsatzTV is started
|
||||
- The template is a [Sub Station Alpha](http://www.tcax.org/docs/ass-specs.htm) file using [scribian](https://github.com/scriban/scriban/tree/master/doc) template syntax
|
||||
- The following fields are available for use in the template:
|
||||
- `resolution`: the ffmpeg profile's resolution, which is used for margin calculations
|
||||
- `title`: the title of the music video
|
||||
- `track`: the music video's track number
|
||||
- `album`: the music video's album
|
||||
- `plot`: the music video's plot
|
||||
- `release_date`: the music video's release date
|
||||
- `artist`: the music videos artist (the parent folder)
|
||||
- `all_artists`: a list of additional artists from the music video's sidecar NFO metadata file
|
||||
- `duration`: the timespan duration of the music video, which can be used to calculate timing of additional subtitles
|
||||
|
||||
## [0.6.8-beta] - 2022-10-05
|
||||
### Fixed
|
||||
- Fix typo introduced in `0.6.7-beta` that stopped QSV HEVC encoder from working
|
||||
|
||||
@@ -18,4 +18,5 @@ public record ChannelViewModel(
|
||||
int PlayoutCount,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode);
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate);
|
||||
|
||||
@@ -18,4 +18,5 @@ public record CreateChannel
|
||||
int? FallbackFillerId,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
|
||||
@@ -74,7 +74,8 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
||||
PreferredAudioTitle = request.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
|
||||
SubtitleMode = request.SubtitleMode,
|
||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode
|
||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
|
||||
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate
|
||||
};
|
||||
|
||||
foreach (int id in watermarkId)
|
||||
|
||||
@@ -19,4 +19,5 @@ public record UpdateChannel
|
||||
int? FallbackFillerId,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
|
||||
@@ -46,6 +46,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
||||
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
|
||||
c.SubtitleMode = update.SubtitleMode;
|
||||
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
||||
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
|
||||
c.Artwork ??= new List<Artwork>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||
|
||||
@@ -22,7 +22,8 @@ internal static class Mapper
|
||||
channel.Playouts?.Count ?? 0,
|
||||
channel.PreferredSubtitleLanguageCode,
|
||||
channel.SubtitleMode,
|
||||
channel.MusicVideoCreditsMode);
|
||||
channel.MusicVideoCreditsMode,
|
||||
channel.MusicVideoCreditsTemplate);
|
||||
|
||||
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
|
||||
new(
|
||||
|
||||
@@ -40,5 +40,6 @@
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=subtitles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=television_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=templates_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Cqueries/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
+23
-11
@@ -324,18 +324,30 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
{
|
||||
var subtitles = new List<Subtitle>();
|
||||
|
||||
bool musicVideoCredits = channel.MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.GenerateSubtitles;
|
||||
if (musicVideoCredits)
|
||||
switch (channel.MusicVideoCreditsMode)
|
||||
{
|
||||
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>()));
|
||||
case ChannelMusicVideoCreditsMode.GenerateSubtitles:
|
||||
subtitles.AddRange(
|
||||
await _musicVideoCreditsGenerator.GenerateCreditsSubtitle(musicVideo, channel.FFmpegProfile));
|
||||
break;
|
||||
case ChannelMusicVideoCreditsMode.TemplateSubtitles:
|
||||
var fileWithExtension = $"{channel.MusicVideoCreditsTemplate}.sbntxt";
|
||||
if (!string.IsNullOrWhiteSpace(fileWithExtension))
|
||||
{
|
||||
subtitles.AddRange(
|
||||
await _musicVideoCreditsGenerator.GenerateCreditsSubtitleFromTemplate(
|
||||
musicVideo,
|
||||
channel.FFmpegProfile,
|
||||
Path.Combine(FileSystemLayout.MusicVideoCreditsTemplatesFolder, fileWithExtension)));
|
||||
}
|
||||
break;
|
||||
case ChannelMusicVideoCreditsMode.None:
|
||||
default:
|
||||
subtitles.AddRange(
|
||||
await Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone()
|
||||
.Map(mm => mm.Subtitles)
|
||||
.IfNoneAsync(new List<Subtitle>()));
|
||||
break;
|
||||
}
|
||||
|
||||
return subtitles;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Templates;
|
||||
|
||||
public record GetMusicVideoCreditTemplates : IRequest<List<string>>;
|
||||
@@ -0,0 +1,20 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
|
||||
namespace ErsatzTV.Application.Templates;
|
||||
|
||||
public class GetMusicVideoCreditTemplatesHandler : IRequestHandler<GetMusicVideoCreditTemplates, List<string>>
|
||||
{
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
|
||||
public GetMusicVideoCreditTemplatesHandler(ILocalFileSystem localFileSystem)
|
||||
{
|
||||
_localFileSystem = localFileSystem;
|
||||
}
|
||||
|
||||
public Task<List<string>> Handle(GetMusicVideoCreditTemplates request, CancellationToken cancellationToken) =>
|
||||
_localFileSystem.ListFiles(FileSystemLayout.MusicVideoCreditsTemplatesFolder)
|
||||
.Map(Path.GetFileNameWithoutExtension)
|
||||
.ToList()
|
||||
.AsTask();
|
||||
}
|
||||
@@ -27,4 +27,5 @@ public class Channel
|
||||
public string PreferredSubtitleLanguageCode { get; set; }
|
||||
public ChannelSubtitleMode SubtitleMode { get; set; }
|
||||
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
||||
public string MusicVideoCreditsTemplate { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
public enum ChannelMusicVideoCreditsMode
|
||||
{
|
||||
None = 0,
|
||||
GenerateSubtitles = 1
|
||||
GenerateSubtitles = 1,
|
||||
TemplateSubtitles = 2
|
||||
}
|
||||
|
||||
@@ -92,7 +92,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
|
||||
string preferredSubtitleLanguage,
|
||||
ChannelSubtitleMode subtitleMode)
|
||||
{
|
||||
if (channel.MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.GenerateSubtitles &&
|
||||
if (channel.MusicVideoCreditsMode is ChannelMusicVideoCreditsMode.GenerateSubtitles
|
||||
or ChannelMusicVideoCreditsMode.TemplateSubtitles &&
|
||||
subtitles.FirstOrDefault(s => s.SubtitleKind == SubtitleKind.Generated) is { } generatedSubtitle)
|
||||
{
|
||||
_logger.LogDebug("Selecting generated subtitle for channel {Number}", channel.Number);
|
||||
|
||||
@@ -45,4 +45,9 @@ public static class FileSystemLayout
|
||||
|
||||
public static readonly string SubtitleCacheFolder = Path.Combine(StreamsCacheFolder, "subtitles");
|
||||
public static readonly string FontsCacheFolder = Path.Combine(StreamsCacheFolder, "fonts");
|
||||
|
||||
public static readonly string TemplatesFolder = Path.Combine(AppDataFolder, "templates");
|
||||
|
||||
public static readonly string MusicVideoCreditsTemplatesFolder =
|
||||
Path.Combine(TemplatesFolder, "music-video-credits");
|
||||
}
|
||||
|
||||
@@ -5,4 +5,9 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
public interface IMusicVideoCreditsGenerator
|
||||
{
|
||||
Task<Option<Subtitle>> GenerateCreditsSubtitle(MusicVideo musicVideo, FFmpegProfile ffmpegProfile);
|
||||
|
||||
Task<Option<Subtitle>> GenerateCreditsSubtitleFromTemplate(
|
||||
MusicVideo musicVideo,
|
||||
FFmpegProfile ffmpegProfile,
|
||||
string templateFileName);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ public class SearchRepository : ISearchRepository
|
||||
.ThenInclude(sm => sm.Studios)
|
||||
.Include(mi => (mi as Season).SeasonMetadata)
|
||||
.ThenInclude(sm => sm.Actors)
|
||||
.Include(mi => (mi as Season).SeasonMetadata)
|
||||
.ThenInclude(sm => sm.Guids)
|
||||
.Include(mi => (mi as Season).Show)
|
||||
.ThenInclude(sm => sm.ShowMetadata)
|
||||
.ThenInclude(sm => sm.Genres)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<PackageReference Include="Refit" Version="6.3.2" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.3.2" />
|
||||
<PackageReference Include="Refit.Xml" Version="6.3.2" />
|
||||
<PackageReference Include="Scriban.Signed" Version="5.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+64
-2
@@ -1,14 +1,23 @@
|
||||
using System.Text;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Scriban;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg;
|
||||
namespace ErsatzTV.Infrastructure.FFmpeg;
|
||||
|
||||
public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
|
||||
{
|
||||
private readonly ITempFilePool _tempFilePool;
|
||||
private readonly ILogger<MusicVideoCreditsGenerator> _logger;
|
||||
|
||||
public MusicVideoCreditsGenerator(ITempFilePool tempFilePool) => _tempFilePool = tempFilePool;
|
||||
public MusicVideoCreditsGenerator(ITempFilePool tempFilePool, ILogger<MusicVideoCreditsGenerator> logger)
|
||||
{
|
||||
_tempFilePool = tempFilePool;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Option<Subtitle>> GenerateCreditsSubtitle(MusicVideo musicVideo, FFmpegProfile ffmpegProfile)
|
||||
{
|
||||
@@ -81,4 +90,57 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
public async Task<Option<Subtitle>> GenerateCreditsSubtitleFromTemplate(
|
||||
MusicVideo musicVideo,
|
||||
FFmpegProfile ffmpegProfile,
|
||||
string templateFileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
string text = await File.ReadAllTextAsync(templateFileName);
|
||||
var template = Template.Parse(text, templateFileName);
|
||||
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata)
|
||||
{
|
||||
string artist = string.Empty;
|
||||
foreach (ArtistMetadata artistMetadata in Optional(musicVideo.Artist?.ArtistMetadata).Flatten())
|
||||
{
|
||||
artist = artistMetadata.Title;
|
||||
}
|
||||
|
||||
string result = await template.RenderAsync(
|
||||
new
|
||||
{
|
||||
ffmpegProfile.Resolution,
|
||||
metadata.Title,
|
||||
metadata.Track,
|
||||
metadata.Album,
|
||||
metadata.Plot,
|
||||
metadata.ReleaseDate,
|
||||
AllArtists = (metadata.Artists ?? new List<MusicVideoArtist>()).Map(a => a.Name),
|
||||
Artist = artist,
|
||||
musicVideo.GetHeadVersion().Duration
|
||||
});
|
||||
|
||||
string fileName = _tempFilePool.GetNextTempFile(TempFileCategory.Subtitle);
|
||||
await File.WriteAllTextAsync(fileName, result);
|
||||
return new Subtitle
|
||||
{
|
||||
Codec = "ass",
|
||||
Default = true,
|
||||
Forced = true,
|
||||
IsExtracted = false,
|
||||
SubtitleKind = SubtitleKind.Generated,
|
||||
Path = fileName,
|
||||
SDH = false
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating music video credits from template {Template}", templateFileName);
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Generated
+4309
File diff suppressed because it is too large
Load Diff
+25
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_ChannelMusicVideoCreditsTemplate : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "MusicVideoCreditsTemplate",
|
||||
table: "Channel",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MusicVideoCreditsTemplate",
|
||||
table: "Channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +236,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int>("MusicVideoCreditsMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("MusicVideoCreditsTemplate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -50,10 +50,6 @@
|
||||
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm ci" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" />
|
||||
@@ -109,6 +105,7 @@
|
||||
<EmbeddedResource Include="Resources\Roboto-Regular.ttf" />
|
||||
<EmbeddedResource Include="Resources\OPTIKabel-Heavy.otf" />
|
||||
<EmbeddedResource Include="Resources\ISO-639-2_utf-8.txt" />
|
||||
<EmbeddedResource Include="Resources\Templates\_default.ass.sbntxt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@using System.Globalization
|
||||
@using ErsatzTV.Core.Domain.Filler
|
||||
@using ErsatzTV.Application.Channels
|
||||
@using ErsatzTV.Application.Templates
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ILogger<ChannelEditor> Logger
|
||||
@@ -75,6 +76,18 @@
|
||||
<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>
|
||||
<MudSelectItem Value="@(ChannelMusicVideoCreditsMode.TemplateSubtitles)">Template Subtitles</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3"
|
||||
Label="Music Video Credits Template"
|
||||
@bind-Value="_model.MusicVideoCreditsTemplate"
|
||||
For="@(() => _model.MusicVideoCreditsTemplate)"
|
||||
Disabled="@(_model.MusicVideoCreditsMode != ChannelMusicVideoCreditsMode.TemplateSubtitles)">
|
||||
<MudSelectItem T="string" Value="@((string)null)">(none)</MudSelectItem>
|
||||
@foreach (string template in _musicVideoCreditsTemplates)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@template">@template</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
|
||||
<MudItem xs="6">
|
||||
@@ -139,6 +152,7 @@
|
||||
private List<CultureInfo> _availableCultures;
|
||||
private List<WatermarkViewModel> _watermarks;
|
||||
private List<FillerPresetViewModel> _fillerPresets;
|
||||
private List<string> _musicVideoCreditsTemplates;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -152,6 +166,7 @@
|
||||
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), _cts.Token);
|
||||
await LoadWatermarks(_cts.Token);
|
||||
await LoadFillerPresets(_cts.Token);
|
||||
await LoadMusicVideoCreditsTemplates(_cts.Token);
|
||||
|
||||
if (Id.HasValue)
|
||||
{
|
||||
@@ -174,6 +189,7 @@
|
||||
_model.PreferredSubtitleLanguageCode = channelViewModel.PreferredSubtitleLanguageCode;
|
||||
_model.SubtitleMode = channelViewModel.SubtitleMode;
|
||||
_model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode;
|
||||
_model.MusicVideoCreditsTemplate = channelViewModel.MusicVideoCreditsTemplate;
|
||||
},
|
||||
() => NavigationManager.NavigateTo("404"));
|
||||
}
|
||||
@@ -211,6 +227,9 @@
|
||||
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), cancellationToken)
|
||||
.Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList());
|
||||
|
||||
private async Task LoadMusicVideoCreditsTemplates(CancellationToken cancellationToken) =>
|
||||
_musicVideoCreditsTemplates = await Mediator.Send(new GetMusicVideoCreditTemplates(), cancellationToken);
|
||||
|
||||
private async Task HandleSubmitAsync()
|
||||
{
|
||||
_messageStore.Clear();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[Script Info]
|
||||
ScriptType: v4.00+
|
||||
WrapStyle: 0
|
||||
ScaledBorderAndShadow: yes
|
||||
YCbCr Matrix: None
|
||||
PlayResX: {{ resolution.width }}
|
||||
PlayResY: {{ resolution.height }}
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BorderStyle, Outline, Shadow, Alignment, Encoding
|
||||
Style: Default,OPTIKabel-Heavy,{{ resolution.height // 20.0 }},&HFFFFFF,&H444444,1,1,3,0,1
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,00:00:09.00,00:00:16.00,Default,,{{ resolution.width * 0.03 | math.round }},{{ resolution.width * 0.03 | math.round }},{{ resolution.height * 0.05 | math.round }},,{fad(1200, 1200)}{{ if all_artists | array.size == 0 }}{{ artist }}{{ else }}{{ all_artists | array.join " / " }}{{ end }}\N"{{ title }}"{{ if album }}\N{{ album }}{{ end }}
|
||||
@@ -22,6 +22,12 @@ public class ResourceExtractorService : IHostedService
|
||||
|
||||
await ExtractFontResource(assembly, "Roboto-Regular.ttf", cancellationToken);
|
||||
await ExtractFontResource(assembly, "OPTIKabel-Heavy.otf", cancellationToken);
|
||||
|
||||
await ExtractTemplateResource(
|
||||
assembly,
|
||||
"_default.ass.sbntxt",
|
||||
FileSystemLayout.MusicVideoCreditsTemplatesFolder,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
@@ -31,8 +37,7 @@ public class ResourceExtractorService : IHostedService
|
||||
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
|
||||
if (resource != null)
|
||||
{
|
||||
await using FileStream fs = File.Create(
|
||||
Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
||||
await using FileStream fs = File.Create(Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
||||
await resource.CopyToAsync(fs, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -42,15 +47,28 @@ public class ResourceExtractorService : IHostedService
|
||||
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
|
||||
if (resource != null)
|
||||
{
|
||||
await using FileStream fs = File.Create(
|
||||
Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
||||
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 using FileStream fontCacheFileStream =
|
||||
File.Create(Path.Combine(FileSystemLayout.FontsCacheFolder, name));
|
||||
await resource.CopyToAsync(fontCacheFileStream, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ExtractTemplateResource(
|
||||
Assembly assembly,
|
||||
string name,
|
||||
string targetFolder,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.Templates.{name}");
|
||||
if (resource != null)
|
||||
{
|
||||
await using FileStream fs = File.Create(Path.Combine(targetFolder, name));
|
||||
await resource.CopyToAsync(fs, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Data.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
|
||||
using ErsatzTV.Infrastructure.Emby;
|
||||
using ErsatzTV.Infrastructure.FFmpeg;
|
||||
using ErsatzTV.Infrastructure.GitHub;
|
||||
using ErsatzTV.Infrastructure.Health;
|
||||
using ErsatzTV.Infrastructure.Health.Checks;
|
||||
@@ -188,6 +189,16 @@ public class Startup
|
||||
Directory.CreateDirectory(FileSystemLayout.FontsCacheFolder);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(FileSystemLayout.TemplatesFolder))
|
||||
{
|
||||
Directory.CreateDirectory(FileSystemLayout.TemplatesFolder);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(FileSystemLayout.MusicVideoCreditsTemplatesFolder))
|
||||
{
|
||||
Directory.CreateDirectory(FileSystemLayout.MusicVideoCreditsTemplatesFolder);
|
||||
}
|
||||
|
||||
Log.Logger.Information("Database is at {DatabasePath}", FileSystemLayout.DatabasePath);
|
||||
|
||||
// until we add a setting for a file-specific scheme://host:port to access
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ErsatzTV.ViewModels;
|
||||
|
||||
public class ChannelEditViewModel
|
||||
{
|
||||
private string _musicVideoCreditsTemplate;
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Group { get; set; }
|
||||
@@ -21,6 +22,14 @@ public class ChannelEditViewModel
|
||||
public ChannelSubtitleMode SubtitleMode { get; set; }
|
||||
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
||||
|
||||
public string MusicVideoCreditsTemplate
|
||||
{
|
||||
get => MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.TemplateSubtitles
|
||||
? _musicVideoCreditsTemplate
|
||||
: null;
|
||||
set => _musicVideoCreditsTemplate = value;
|
||||
}
|
||||
|
||||
public UpdateChannel ToUpdate() =>
|
||||
new(
|
||||
Id,
|
||||
@@ -37,7 +46,8 @@ public class ChannelEditViewModel
|
||||
FallbackFillerId,
|
||||
PreferredSubtitleLanguageCode,
|
||||
SubtitleMode,
|
||||
MusicVideoCreditsMode);
|
||||
MusicVideoCreditsMode,
|
||||
MusicVideoCreditsTemplate);
|
||||
|
||||
public CreateChannel ToCreate() =>
|
||||
new(
|
||||
@@ -54,5 +64,6 @@ public class ChannelEditViewModel
|
||||
FallbackFillerId,
|
||||
PreferredSubtitleLanguageCode,
|
||||
SubtitleMode,
|
||||
MusicVideoCreditsMode);
|
||||
MusicVideoCreditsMode,
|
||||
MusicVideoCreditsTemplate);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user