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
|
- 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
|
- 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
|
## [0.6.8-beta] - 2022-10-05
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix typo introduced in `0.6.7-beta` that stopped QSV HEVC encoder from working
|
- Fix typo introduced in `0.6.7-beta` that stopped QSV HEVC encoder from working
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ public record ChannelViewModel(
|
|||||||
int PlayoutCount,
|
int PlayoutCount,
|
||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode SubtitleMode,
|
ChannelSubtitleMode SubtitleMode,
|
||||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode);
|
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||||
|
string MusicVideoCreditsTemplate);
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ public record CreateChannel
|
|||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode SubtitleMode,
|
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,
|
PreferredAudioTitle = request.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
|
||||||
SubtitleMode = request.SubtitleMode,
|
SubtitleMode = request.SubtitleMode,
|
||||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode
|
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
|
||||||
|
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (int id in watermarkId)
|
foreach (int id in watermarkId)
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ public record UpdateChannel
|
|||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode SubtitleMode,
|
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.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
|
||||||
c.SubtitleMode = update.SubtitleMode;
|
c.SubtitleMode = update.SubtitleMode;
|
||||||
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
||||||
|
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
|
||||||
c.Artwork ??= new List<Artwork>();
|
c.Artwork ??= new List<Artwork>();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ internal static class Mapper
|
|||||||
channel.Playouts?.Count ?? 0,
|
channel.Playouts?.Count ?? 0,
|
||||||
channel.PreferredSubtitleLanguageCode,
|
channel.PreferredSubtitleLanguageCode,
|
||||||
channel.SubtitleMode,
|
channel.SubtitleMode,
|
||||||
channel.MusicVideoCreditsMode);
|
channel.MusicVideoCreditsMode,
|
||||||
|
channel.MusicVideoCreditsTemplate);
|
||||||
|
|
||||||
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
|
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
|
||||||
new(
|
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/=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/=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/=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_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Cqueries/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<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>();
|
var subtitles = new List<Subtitle>();
|
||||||
|
|
||||||
bool musicVideoCredits = channel.MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.GenerateSubtitles;
|
switch (channel.MusicVideoCreditsMode)
|
||||||
if (musicVideoCredits)
|
|
||||||
{
|
{
|
||||||
subtitles.AddRange(
|
case ChannelMusicVideoCreditsMode.GenerateSubtitles:
|
||||||
await _musicVideoCreditsGenerator.GenerateCreditsSubtitle(musicVideo, channel.FFmpegProfile));
|
subtitles.AddRange(
|
||||||
}
|
await _musicVideoCreditsGenerator.GenerateCreditsSubtitle(musicVideo, channel.FFmpegProfile));
|
||||||
else
|
break;
|
||||||
{
|
case ChannelMusicVideoCreditsMode.TemplateSubtitles:
|
||||||
subtitles.AddRange(
|
var fileWithExtension = $"{channel.MusicVideoCreditsTemplate}.sbntxt";
|
||||||
await Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone()
|
if (!string.IsNullOrWhiteSpace(fileWithExtension))
|
||||||
.Map(mm => mm.Subtitles)
|
{
|
||||||
.IfNoneAsync(new List<Subtitle>()));
|
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;
|
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 string PreferredSubtitleLanguageCode { get; set; }
|
||||||
public ChannelSubtitleMode SubtitleMode { get; set; }
|
public ChannelSubtitleMode SubtitleMode { get; set; }
|
||||||
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
||||||
|
public string MusicVideoCreditsTemplate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
public enum ChannelMusicVideoCreditsMode
|
public enum ChannelMusicVideoCreditsMode
|
||||||
{
|
{
|
||||||
None = 0,
|
None = 0,
|
||||||
GenerateSubtitles = 1
|
GenerateSubtitles = 1,
|
||||||
|
TemplateSubtitles = 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
|
|||||||
string preferredSubtitleLanguage,
|
string preferredSubtitleLanguage,
|
||||||
ChannelSubtitleMode subtitleMode)
|
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)
|
subtitles.FirstOrDefault(s => s.SubtitleKind == SubtitleKind.Generated) is { } generatedSubtitle)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Selecting generated subtitle for channel {Number}", channel.Number);
|
_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 SubtitleCacheFolder = Path.Combine(StreamsCacheFolder, "subtitles");
|
||||||
public static readonly string FontsCacheFolder = Path.Combine(StreamsCacheFolder, "fonts");
|
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
|
public interface IMusicVideoCreditsGenerator
|
||||||
{
|
{
|
||||||
Task<Option<Subtitle>> GenerateCreditsSubtitle(MusicVideo musicVideo, FFmpegProfile ffmpegProfile);
|
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)
|
.ThenInclude(sm => sm.Studios)
|
||||||
.Include(mi => (mi as Season).SeasonMetadata)
|
.Include(mi => (mi as Season).SeasonMetadata)
|
||||||
.ThenInclude(sm => sm.Actors)
|
.ThenInclude(sm => sm.Actors)
|
||||||
|
.Include(mi => (mi as Season).SeasonMetadata)
|
||||||
|
.ThenInclude(sm => sm.Guids)
|
||||||
.Include(mi => (mi as Season).Show)
|
.Include(mi => (mi as Season).Show)
|
||||||
.ThenInclude(sm => sm.ShowMetadata)
|
.ThenInclude(sm => sm.ShowMetadata)
|
||||||
.ThenInclude(sm => sm.Genres)
|
.ThenInclude(sm => sm.Genres)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<PackageReference Include="Refit" Version="6.3.2" />
|
<PackageReference Include="Refit" Version="6.3.2" />
|
||||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.3.2" />
|
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.3.2" />
|
||||||
<PackageReference Include="Refit.Xml" Version="6.3.2" />
|
<PackageReference Include="Refit.Xml" Version="6.3.2" />
|
||||||
|
<PackageReference Include="Scriban.Signed" Version="5.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
+64
-2
@@ -1,14 +1,23 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
|
using ErsatzTV.Core.FFmpeg;
|
||||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Scriban;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.FFmpeg;
|
namespace ErsatzTV.Infrastructure.FFmpeg;
|
||||||
|
|
||||||
public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
|
public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
|
||||||
{
|
{
|
||||||
private readonly ITempFilePool _tempFilePool;
|
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)
|
public async Task<Option<Subtitle>> GenerateCreditsSubtitle(MusicVideo musicVideo, FFmpegProfile ffmpegProfile)
|
||||||
{
|
{
|
||||||
@@ -81,4 +90,57 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
|
|||||||
|
|
||||||
return None;
|
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")
|
b.Property<int>("MusicVideoCreditsMode")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("MusicVideoCreditsTemplate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,6 @@
|
|||||||
|
|
||||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm ci" />
|
<Exec WorkingDirectory="$(SpaRoot)" Command="npm ci" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Resources" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" />
|
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" />
|
||||||
@@ -109,6 +105,7 @@
|
|||||||
<EmbeddedResource Include="Resources\Roboto-Regular.ttf" />
|
<EmbeddedResource Include="Resources\Roboto-Regular.ttf" />
|
||||||
<EmbeddedResource Include="Resources\OPTIKabel-Heavy.otf" />
|
<EmbeddedResource Include="Resources\OPTIKabel-Heavy.otf" />
|
||||||
<EmbeddedResource Include="Resources\ISO-639-2_utf-8.txt" />
|
<EmbeddedResource Include="Resources\ISO-639-2_utf-8.txt" />
|
||||||
|
<EmbeddedResource Include="Resources\Templates\_default.ass.sbntxt" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@using ErsatzTV.Core.Domain.Filler
|
@using ErsatzTV.Core.Domain.Filler
|
||||||
@using ErsatzTV.Application.Channels
|
@using ErsatzTV.Application.Channels
|
||||||
|
@using ErsatzTV.Application.Templates
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject ILogger<ChannelEditor> Logger
|
@inject ILogger<ChannelEditor> Logger
|
||||||
@@ -75,6 +76,18 @@
|
|||||||
<MudSelect Class="mt-3" Label="Music Video Credits Mode" @bind-Value="_model.MusicVideoCreditsMode" For="@(() => _model.MusicVideoCreditsMode)">
|
<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.None)">None</MudSelectItem>
|
||||||
<MudSelectItem Value="@(ChannelMusicVideoCreditsMode.GenerateSubtitles)">Generate Subtitles</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>
|
</MudSelect>
|
||||||
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
|
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
|
||||||
<MudItem xs="6">
|
<MudItem xs="6">
|
||||||
@@ -139,6 +152,7 @@
|
|||||||
private List<CultureInfo> _availableCultures;
|
private List<CultureInfo> _availableCultures;
|
||||||
private List<WatermarkViewModel> _watermarks;
|
private List<WatermarkViewModel> _watermarks;
|
||||||
private List<FillerPresetViewModel> _fillerPresets;
|
private List<FillerPresetViewModel> _fillerPresets;
|
||||||
|
private List<string> _musicVideoCreditsTemplates;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -152,6 +166,7 @@
|
|||||||
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), _cts.Token);
|
_availableCultures = await Mediator.Send(new GetAllLanguageCodes(), _cts.Token);
|
||||||
await LoadWatermarks(_cts.Token);
|
await LoadWatermarks(_cts.Token);
|
||||||
await LoadFillerPresets(_cts.Token);
|
await LoadFillerPresets(_cts.Token);
|
||||||
|
await LoadMusicVideoCreditsTemplates(_cts.Token);
|
||||||
|
|
||||||
if (Id.HasValue)
|
if (Id.HasValue)
|
||||||
{
|
{
|
||||||
@@ -174,6 +189,7 @@
|
|||||||
_model.PreferredSubtitleLanguageCode = channelViewModel.PreferredSubtitleLanguageCode;
|
_model.PreferredSubtitleLanguageCode = channelViewModel.PreferredSubtitleLanguageCode;
|
||||||
_model.SubtitleMode = channelViewModel.SubtitleMode;
|
_model.SubtitleMode = channelViewModel.SubtitleMode;
|
||||||
_model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode;
|
_model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode;
|
||||||
|
_model.MusicVideoCreditsTemplate = channelViewModel.MusicVideoCreditsTemplate;
|
||||||
},
|
},
|
||||||
() => NavigationManager.NavigateTo("404"));
|
() => NavigationManager.NavigateTo("404"));
|
||||||
}
|
}
|
||||||
@@ -211,6 +227,9 @@
|
|||||||
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), cancellationToken)
|
_fillerPresets = await Mediator.Send(new GetAllFillerPresets(), cancellationToken)
|
||||||
.Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList());
|
.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()
|
private async Task HandleSubmitAsync()
|
||||||
{
|
{
|
||||||
_messageStore.Clear();
|
_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, "Roboto-Regular.ttf", cancellationToken);
|
||||||
await ExtractFontResource(assembly, "OPTIKabel-Heavy.otf", cancellationToken);
|
await ExtractFontResource(assembly, "OPTIKabel-Heavy.otf", cancellationToken);
|
||||||
|
|
||||||
|
await ExtractTemplateResource(
|
||||||
|
assembly,
|
||||||
|
"_default.ass.sbntxt",
|
||||||
|
FileSystemLayout.MusicVideoCreditsTemplatesFolder,
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
@@ -31,8 +37,7 @@ public class ResourceExtractorService : IHostedService
|
|||||||
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
|
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
|
||||||
if (resource != null)
|
if (resource != null)
|
||||||
{
|
{
|
||||||
await using FileStream fs = File.Create(
|
await using FileStream fs = File.Create(Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
||||||
Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
|
||||||
await resource.CopyToAsync(fs, cancellationToken);
|
await resource.CopyToAsync(fs, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,15 +47,28 @@ public class ResourceExtractorService : IHostedService
|
|||||||
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
|
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
|
||||||
if (resource != null)
|
if (resource != null)
|
||||||
{
|
{
|
||||||
await using FileStream fs = File.Create(
|
await using FileStream fs = File.Create(Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
||||||
Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
|
|
||||||
await resource.CopyToAsync(fs, cancellationToken);
|
await resource.CopyToAsync(fs, cancellationToken);
|
||||||
|
|
||||||
resource.Position = 0;
|
resource.Position = 0;
|
||||||
|
|
||||||
await using FileStream fontCacheFileStream = File.Create(
|
await using FileStream fontCacheFileStream =
|
||||||
Path.Combine(FileSystemLayout.FontsCacheFolder, name));
|
File.Create(Path.Combine(FileSystemLayout.FontsCacheFolder, name));
|
||||||
await resource.CopyToAsync(fontCacheFileStream, cancellationToken);
|
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;
|
||||||
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
|
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
|
||||||
using ErsatzTV.Infrastructure.Emby;
|
using ErsatzTV.Infrastructure.Emby;
|
||||||
|
using ErsatzTV.Infrastructure.FFmpeg;
|
||||||
using ErsatzTV.Infrastructure.GitHub;
|
using ErsatzTV.Infrastructure.GitHub;
|
||||||
using ErsatzTV.Infrastructure.Health;
|
using ErsatzTV.Infrastructure.Health;
|
||||||
using ErsatzTV.Infrastructure.Health.Checks;
|
using ErsatzTV.Infrastructure.Health.Checks;
|
||||||
@@ -188,6 +189,16 @@ public class Startup
|
|||||||
Directory.CreateDirectory(FileSystemLayout.FontsCacheFolder);
|
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);
|
Log.Logger.Information("Database is at {DatabasePath}", FileSystemLayout.DatabasePath);
|
||||||
|
|
||||||
// until we add a setting for a file-specific scheme://host:port to access
|
// until we add a setting for a file-specific scheme://host:port to access
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace ErsatzTV.ViewModels;
|
|||||||
|
|
||||||
public class ChannelEditViewModel
|
public class ChannelEditViewModel
|
||||||
{
|
{
|
||||||
|
private string _musicVideoCreditsTemplate;
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Group { get; set; }
|
public string Group { get; set; }
|
||||||
@@ -21,6 +22,14 @@ public class ChannelEditViewModel
|
|||||||
public ChannelSubtitleMode SubtitleMode { get; set; }
|
public ChannelSubtitleMode SubtitleMode { get; set; }
|
||||||
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
||||||
|
|
||||||
|
public string MusicVideoCreditsTemplate
|
||||||
|
{
|
||||||
|
get => MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.TemplateSubtitles
|
||||||
|
? _musicVideoCreditsTemplate
|
||||||
|
: null;
|
||||||
|
set => _musicVideoCreditsTemplate = value;
|
||||||
|
}
|
||||||
|
|
||||||
public UpdateChannel ToUpdate() =>
|
public UpdateChannel ToUpdate() =>
|
||||||
new(
|
new(
|
||||||
Id,
|
Id,
|
||||||
@@ -37,7 +46,8 @@ public class ChannelEditViewModel
|
|||||||
FallbackFillerId,
|
FallbackFillerId,
|
||||||
PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode,
|
SubtitleMode,
|
||||||
MusicVideoCreditsMode);
|
MusicVideoCreditsMode,
|
||||||
|
MusicVideoCreditsTemplate);
|
||||||
|
|
||||||
public CreateChannel ToCreate() =>
|
public CreateChannel ToCreate() =>
|
||||||
new(
|
new(
|
||||||
@@ -54,5 +64,6 @@ public class ChannelEditViewModel
|
|||||||
FallbackFillerId,
|
FallbackFillerId,
|
||||||
PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode,
|
SubtitleMode,
|
||||||
MusicVideoCreditsMode);
|
MusicVideoCreditsMode,
|
||||||
|
MusicVideoCreditsTemplate);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user