Browse Source

add endpoint for generating music video credits

pull/2917/head
Jason Dove 2 months ago
parent
commit
1e296a91b3
No known key found for this signature in database
  1. 1
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Streaming/Queries/GetMusicVideoCreditsByPlayoutItemId.cs
  3. 94
      ErsatzTV.Application/Streaming/Queries/GetMusicVideoCreditsByPlayoutItemIdHandler.cs
  4. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  5. 2
      ErsatzTV.Core/Interfaces/FFmpeg/IMusicVideoCreditsGenerator.cs
  6. 10
      ErsatzTV.Infrastructure/FFmpeg/MusicVideoCreditsGenerator.cs
  7. 17
      ErsatzTV/Controllers/InternalController.cs

1
CHANGELOG.md

@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix HLS Direct playback when JWT auth is also used - Fix HLS Direct playback when JWT auth is also used
- Use configured ffmpeg path for motion and subtitle graphics elements - Use configured ffmpeg path for motion and subtitle graphics elements
- Previously, these elements required ffmpeg to be on PATH - Previously, these elements required ffmpeg to be on PATH
- Fix erroneous warning `Unable to locate MPEG-TS Script in folder Default` on installations with case-sensitive file systems
## [26.5.1] - 2026-05-08 ## [26.5.1] - 2026-05-08
### Fixed ### Fixed

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

@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Streaming;
public record GetMusicVideoCreditsByPlayoutItemId(int PlayoutItemId, Option<long> SeekToMs) : IRequest<Option<string>>;

94
ErsatzTV.Application/Streaming/Queries/GetMusicVideoCreditsByPlayoutItemIdHandler.cs

@ -0,0 +1,94 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Streaming;
public class GetMusicVideoCreditsByPlayoutItemIdHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMusicVideoCreditsGenerator musicVideoCreditsGenerator,
ILogger<GetMusicVideoCreditsByPlayoutItemIdHandler> logger)
: IRequestHandler<GetMusicVideoCreditsByPlayoutItemId, Option<string>>
{
public async Task<Option<string>> Handle(
GetMusicVideoCreditsByPlayoutItemId request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<PlayoutItem> maybePlayoutItem = await dbContext.PlayoutItems
.AsNoTracking()
.Include(pi => pi.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(mvm => mvm.Subtitles)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(mvm => mvm.Artists)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(mvm => mvm.Studios)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(mvm => mvm.Directors)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaItem)
.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(pi => pi.Playout)
.ThenInclude(p => p.Channel)
.ThenInclude(c => c.FFmpegProfile)
.ThenInclude(ff => ff.Resolution)
.SingleOrDefaultAsync(pi => pi.Id == request.PlayoutItemId, cancellationToken)
.Map(Optional);
var subtitles = new List<Subtitle>();
foreach (PlayoutItem playoutItem in maybePlayoutItem)
{
if (playoutItem.MediaItem is not MusicVideo musicVideo)
{
break;
}
switch (playoutItem.Playout.Channel.MusicVideoCreditsMode)
{
case ChannelMusicVideoCreditsMode.GenerateSubtitles:
var fileWithExtension = $"{playoutItem.Playout.Channel.MusicVideoCreditsTemplate}.sbntxt";
if (!string.IsNullOrWhiteSpace(fileWithExtension))
{
subtitles.AddRange(
await musicVideoCreditsGenerator.GenerateCreditsSubtitleFromTemplate(
musicVideo,
playoutItem.Playout.Channel.FFmpegProfile,
request.SeekToMs.Map(TimeSpan.FromMilliseconds),
Path.Combine(FileSystemLayout.MusicVideoCreditsTemplatesFolder, fileWithExtension)));
}
else
{
logger.LogWarning(
"Music video credits template {Template} does not exist; falling back to built-in template",
fileWithExtension);
subtitles.AddRange(
await musicVideoCreditsGenerator.GenerateCreditsSubtitle(
musicVideo,
playoutItem.Playout.Channel.FFmpegProfile));
}
break;
case ChannelMusicVideoCreditsMode.None:
default:
break;
}
}
return subtitles.HeadOrNone().Map(s => s.Path);
}
}

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

@ -654,7 +654,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
await _musicVideoCreditsGenerator.GenerateCreditsSubtitleFromTemplate( await _musicVideoCreditsGenerator.GenerateCreditsSubtitleFromTemplate(
musicVideo, musicVideo,
channel.FFmpegProfile, channel.FFmpegProfile,
settings, settings.StreamSeek,
Path.Combine(FileSystemLayout.MusicVideoCreditsTemplatesFolder, fileWithExtension))); Path.Combine(FileSystemLayout.MusicVideoCreditsTemplatesFolder, fileWithExtension)));
} }
else else

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

@ -10,6 +10,6 @@ public interface IMusicVideoCreditsGenerator
Task<Option<Subtitle>> GenerateCreditsSubtitleFromTemplate( Task<Option<Subtitle>> GenerateCreditsSubtitleFromTemplate(
MusicVideo musicVideo, MusicVideo musicVideo,
FFmpegProfile ffmpegProfile, FFmpegProfile ffmpegProfile,
FFmpegPlaybackSettings settings, Option<TimeSpan> streamSeek,
string templateFileName); string templateFileName);
} }

10
ErsatzTV.Infrastructure/FFmpeg/MusicVideoCreditsGenerator.cs

@ -87,7 +87,7 @@ public class MusicVideoCreditsGenerator(ITempFilePool tempFilePool, ILogger<Musi
public async Task<Option<Subtitle>> GenerateCreditsSubtitleFromTemplate( public async Task<Option<Subtitle>> GenerateCreditsSubtitleFromTemplate(
MusicVideo musicVideo, MusicVideo musicVideo,
FFmpegProfile ffmpegProfile, FFmpegProfile ffmpegProfile,
FFmpegPlaybackSettings settings, Option<TimeSpan> streamSeek,
string templateFileName) string templateFileName)
{ {
try try
@ -111,12 +111,12 @@ public class MusicVideoCreditsGenerator(ITempFilePool tempFilePool, ILogger<Musi
metadata.Album, metadata.Album,
metadata.Plot, metadata.Plot,
metadata.ReleaseDate, metadata.ReleaseDate,
AllArtists = (metadata.Artists ?? new List<MusicVideoArtist>()).Map(a => a.Name), AllArtists = (metadata.Artists ?? []).Map(a => a.Name),
Artist = artist, Artist = artist,
Studios = (metadata.Studios ?? new List<Studio>()).Map(s => s.Name), Studios = (metadata.Studios ?? []).Map(s => s.Name),
Directors = (metadata.Directors ?? new List<Director>()).Map(s => s.Name), Directors = (metadata.Directors ?? []).Map(s => s.Name),
musicVideo.GetHeadVersion().Duration, musicVideo.GetHeadVersion().Duration,
StreamSeek = await settings.StreamSeek.IfNoneAsync(TimeSpan.Zero) StreamSeek = await streamSeek.IfNoneAsync(TimeSpan.Zero)
}); });
string fileName = tempFilePool.GetNextTempFile(TempFileCategory.Subtitle); string fileName = tempFilePool.GetNextTempFile(TempFileCategory.Subtitle);

17
ErsatzTV/Controllers/InternalController.cs

@ -46,6 +46,23 @@ public class InternalController : StreamingControllerBase
[HttpGet("ffmpeg/stream/{channelNumber}")] [HttpGet("ffmpeg/stream/{channelNumber}")]
public Task<IActionResult> GetStream(string channelNumber) => GetTsLegacyStream(channelNumber); public Task<IActionResult> GetStream(string channelNumber) => GetTsLegacyStream(channelNumber);
[HttpGet("ffmpeg/music-video-credits/{playoutItemId:int}")]
public async Task<IActionResult> GetMusicVideoCredits(
int playoutItemId,
[FromQuery] long? seekToMs,
CancellationToken cancellationToken)
{
Option<string> maybeCreditsFile = await _mediator.Send(
new GetMusicVideoCreditsByPlayoutItemId(playoutItemId, Optional(seekToMs)),
cancellationToken);
foreach (string creditsFile in maybeCreditsFile)
{
return new PhysicalFileResult(creditsFile, "text/x-ssa");
}
return NotFound();
}
[HttpGet("ffmpeg/remote-stream/{remoteStreamId}")] [HttpGet("ffmpeg/remote-stream/{remoteStreamId}")]
public async Task<IActionResult> GetRemoteStream(int remoteStreamId, CancellationToken cancellationToken) public async Task<IActionResult> GetRemoteStream(int remoteStreamId, CancellationToken cancellationToken)
{ {

Loading…
Cancel
Save