Browse Source

music video credits tweaks (#834)

* fix song subtitles

* always use generated subtitles

* file not found/unavailable fixes
pull/835/head
Jason Dove 3 years ago committed by GitHub
parent
commit
bf3f16451b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 13
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  3. 16
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  4. 4
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  5. 4
      ErsatzTV.Core/Domain/Metadata/SubtitleKind.cs
  6. 4
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  7. 17
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  8. 10
      ErsatzTV.Core/FFmpeg/MusicVideoCreditsGenerator.cs
  9. 11
      ErsatzTV.Core/FFmpeg/SubtitleBuilder.cs
  10. 4
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegStreamSelector.cs
  11. 2
      ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs
  12. 5
      ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs
  13. 15
      ErsatzTV.Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  14. 30
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  15. 30
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  16. 30
      ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs

1
CHANGELOG.md

@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Fix Jellyfin show library paging
- Properly locate and identify multiple Plex servers
- Properly restore `Unavailable`/`File Not Found` items when they are located on disk
### Added
- Add basic music video credits subtitle generation

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

@ -36,7 +36,6 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr @@ -36,7 +36,6 @@ 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(
@ -46,7 +45,6 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr @@ -46,7 +45,6 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
ffmpegProfileId,
preferredAudioLanguageCode,
preferredSubtitleLanguageCode,
_,
watermarkId,
fillerPresetId) =>
{
@ -109,17 +107,6 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr @@ -109,17 +107,6 @@ 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)

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

@ -93,9 +93,8 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr @@ -93,9 +93,8 @@ 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),
ValidateSubtitleAndMusicCredits(request))
.Apply((channelToUpdate, _, _, _, _) => channelToUpdate);
ValidatePreferredAudioLanguage(request))
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
@ -137,15 +136,4 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr @@ -137,15 +136,4 @@ 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;
}
}

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

@ -566,10 +566,8 @@ public class TranscodingTests @@ -566,10 +566,8 @@ public class TranscodingTests
Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
MediaVersion version,
List<Domain.Subtitle> subtitles,
StreamingMode streamingMode,
string channelNumber,
Channel channel,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode) =>
subtitles.HeadOrNone().AsTask();

4
ErsatzTV.Core/Domain/Metadata/SubtitleKind.cs

@ -3,5 +3,7 @@ @@ -3,5 +3,7 @@
public enum SubtitleKind
{
Embedded = 0,
Sidecar = 1
Sidecar = 1,
Generated = 99
}

4
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -71,10 +71,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -71,10 +71,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
preferredAudioLanguage);
Option<Subtitle> maybeSubtitle =
await _ffmpegStreamSelector.SelectSubtitleStream(
videoVersion,
subtitles,
channel.StreamingMode,
channel.Number,
channel,
preferredSubtitleLanguage,
subtitleMode);

17
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -86,19 +86,24 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -86,19 +86,24 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
}
public async Task<Option<Subtitle>> SelectSubtitleStream(
MediaVersion version,
List<Subtitle> subtitles,
StreamingMode streamingMode,
string channelNumber,
Channel channel,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode)
{
if (channel.MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.GenerateSubtitles &&
subtitles.FirstOrDefault(s => s.SubtitleKind == SubtitleKind.Generated) is { } generatedSubtitle)
{
_logger.LogDebug("Selecting generated subtitle for channel {Number}", channel.Number);
return Optional(generatedSubtitle);
}
if (subtitleMode == ChannelSubtitleMode.None)
{
return None;
}
if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(preferredSubtitleLanguage))
{
// _logger.LogDebug(
@ -110,7 +115,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -110,7 +115,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
string language = (preferredSubtitleLanguage ?? string.Empty).ToLowerInvariant();
if (string.IsNullOrWhiteSpace(language))
{
_logger.LogDebug("Channel {Number} has no preferred subtitle language code", channelNumber);
_logger.LogDebug("Channel {Number} has no preferred subtitle language code", channel.Number);
}
else
{
@ -152,7 +157,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -152,7 +157,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
_logger.LogDebug(
"Found no subtitles for channel {ChannelNumber} with mode {Mode} matching language {Language}",
channelNumber,
channel.Number,
subtitleMode,
preferredSubtitleLanguage);

10
ErsatzTV.Core/FFmpeg/MusicVideoCreditsGenerator.cs

@ -64,12 +64,18 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator @@ -64,12 +64,18 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
.WithShadow(3)
.WithFormattedContent(sb.ToString())
.WithStartEnd(TimeSpan.FromSeconds(9), TimeSpan.FromSeconds(16))
.WithFade(true)
.BuildFile();
return new Subtitle
{
Codec = "ass", Default = true, Forced = true, IsExtracted = false, SubtitleKind = SubtitleKind.Sidecar,
Path = subtitles, SDH = false
Codec = "ass",
Default = true,
Forced = true,
IsExtracted = false,
SubtitleKind = SubtitleKind.Generated,
Path = subtitles,
SDH = false
};
}

11
ErsatzTV.Core/FFmpeg/SubtitleBuilder.cs

@ -10,6 +10,7 @@ public class SubtitleBuilder @@ -10,6 +10,7 @@ public class SubtitleBuilder
private Option<int> _borderStyle;
private string _content;
private Option<TimeSpan> _end;
private bool _fade;
private Option<string> _fontName;
private Option<int> _fontSize;
private int _marginLeft;
@ -102,6 +103,12 @@ public class SubtitleBuilder @@ -102,6 +103,12 @@ public class SubtitleBuilder
return this;
}
public SubtitleBuilder WithFade(bool fade)
{
_fade = fade;
return this;
}
public async Task<string> BuildFile()
{
string fileName = _tempFilePool.GetNextTempFile(TempFileCategory.Subtitle);
@ -137,10 +144,12 @@ public class SubtitleBuilder @@ -137,10 +144,12 @@ public class SubtitleBuilder
end = $"{(int)endTime.TotalHours:00}:{endTime.ToString(@"mm\:ss\.ff")}";
}
string fade = _fade ? @"{\fad(1200, 1200)}" : string.Empty;
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}");
@$"Dialogue: 0,{start},{end},Default,,{_marginLeft},{_marginRight},{_marginV},,{fade}{_content}");
await File.WriteAllTextAsync(fileName, sb.ToString());

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

@ -13,10 +13,8 @@ public interface IFFmpegStreamSelector @@ -13,10 +13,8 @@ public interface IFFmpegStreamSelector
string preferredAudioLanguage);
Task<Option<Subtitle>> SelectSubtitleStream(
MediaVersion version,
List<Subtitle> subtitles,
StreamingMode streamingMode,
string channelNumber,
Channel channel,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode);
}

2
ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs

@ -19,6 +19,8 @@ public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, T @@ -19,6 +19,8 @@ public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, T
Task<Unit> SetEtag(TSeason season, string etag);
Task<Unit> SetEtag(TEpisode episode, string etag);
Task<bool> FlagNormal(TLibrary library, TEpisode episode);
Task<bool> FlagNormal(TLibrary library, TSeason season);
Task<bool> FlagNormal(TLibrary library, TShow show);
Task<List<int>> FlagFileNotFoundShows(TLibrary library, List<string> showItemIds);
Task<List<int>> FlagFileNotFoundSeasons(TLibrary library, List<string> seasonItemIds);
Task<List<int>> FlagFileNotFoundEpisodes(TLibrary library, List<string> episodeItemIds);

5
ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -224,9 +224,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -224,9 +224,10 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
string existingEtag = await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty);
MediaItemState existingState = await maybeExisting.Map(e => e.State).IfNoneAsync(MediaItemState.Normal);
if (existingState == MediaItemState.Unavailable && existingEtag == MediaServerEtag(incoming))
if (existingState is MediaItemState.Unavailable or MediaItemState.FileNotFound &&
existingEtag == MediaServerEtag(incoming))
{
// skip scanning unavailable items that are unchanged and still don't exist locally
// skip scanning unavailable/file not found items that are unchanged and still don't exist locally
if (!_localFileSystem.FileExists(localPath))
{
return false;

15
ErsatzTV.Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -189,6 +189,11 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -189,6 +189,11 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming));
if (await televisionRepository.FlagNormal(library, result.Item))
{
result.IsUpdated = true;
}
if (result.IsAdded || result.IsUpdated)
{
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id });
@ -344,6 +349,11 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -344,6 +349,11 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming));
if (await televisionRepository.FlagNormal(library, result.Item))
{
result.IsUpdated = true;
}
result.Item.Show = show;
if (result.IsAdded || result.IsUpdated)
@ -484,9 +494,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -484,9 +494,10 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
string existingEtag = await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty);
MediaItemState existingState = await maybeExisting.Map(e => e.State).IfNoneAsync(MediaItemState.Normal);
if (existingState == MediaItemState.Unavailable && existingEtag == MediaServerEtag(incoming))
if (existingState is MediaItemState.Unavailable or MediaItemState.FileNotFound &&
existingEtag == MediaServerEtag(incoming))
{
// skip scanning unavailable items that are unchanged and still don't exist locally
// skip scanning unavailable/file not found items that are unchanged and still don't exist locally
if (!_localFileSystem.FileExists(localPath))
{
return false;

30
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -209,6 +209,36 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -209,6 +209,36 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
new { LibraryId = library.Id, episode.ItemId }).Map(count => count > 0);
}
public async Task<bool> FlagNormal(EmbyLibrary library, EmbySeason season)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
season.State = MediaItemState.Normal;
return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id IN
(SELECT EmbySeason.Id FROM EmbySeason
INNER JOIN MediaItem MI ON MI.Id = EmbySeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbySeason.ItemId = @ItemId)",
new { LibraryId = library.Id, season.ItemId }).Map(count => count > 0);
}
public async Task<bool> FlagNormal(EmbyLibrary library, EmbyShow show)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal;
return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id IN
(SELECT EmbyShow.Id FROM EmbyShow
INNER JOIN MediaItem MI ON MI.Id = EmbyShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE EmbyShow.ItemId = @ItemId)",
new { LibraryId = library.Id, show.ItemId }).Map(count => count > 0);
}
public async Task<List<int>> FlagFileNotFoundShows(EmbyLibrary library, List<string> showItemIds)
{
if (showItemIds.Count == 0)

30
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -213,6 +213,36 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -213,6 +213,36 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
new { LibraryId = library.Id, episode.ItemId }).Map(count => count > 0);
}
public async Task<bool> FlagNormal(JellyfinLibrary library, JellyfinSeason season)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
season.State = MediaItemState.Normal;
return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id IN
(SELECT JellyfinSeason.Id FROM JellyfinSeason
INNER JOIN MediaItem MI ON MI.Id = JellyfinSeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinSeason.ItemId = @ItemId)",
new { LibraryId = library.Id, season.ItemId }).Map(count => count > 0);
}
public async Task<bool> FlagNormal(JellyfinLibrary library, JellyfinShow show)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal;
return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id IN
(SELECT JellyfinShow.Id FROM JellyfinShow
INNER JOIN MediaItem MI ON MI.Id = JellyfinShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE JellyfinShow.ItemId = @ItemId)",
new { LibraryId = library.Id, show.ItemId }).Map(count => count > 0);
}
public async Task<List<int>> FlagFileNotFoundShows(JellyfinLibrary library, List<string> showItemIds)
{
if (showItemIds.Count == 0)

30
ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs

@ -31,6 +31,36 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -31,6 +31,36 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
new { LibraryId = library.Id, episode.Key }).Map(count => count > 0);
}
public async Task<bool> FlagNormal(PlexLibrary library, PlexSeason season)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
season.State = MediaItemState.Normal;
return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id IN
(SELECT PlexSeason.Id FROM PlexSeason
INNER JOIN MediaItem MI ON MI.Id = PlexSeason.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexSeason.Key = @Key)",
new { LibraryId = library.Id, season.Key }).Map(count => count > 0);
}
public async Task<bool> FlagNormal(PlexLibrary library, PlexShow show)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal;
return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id IN
(SELECT PlexShow.Id FROM PlexShow
INNER JOIN MediaItem MI ON MI.Id = PlexShow.Id
INNER JOIN LibraryPath LP on MI.LibraryPathId = LP.Id AND LibraryId = @LibraryId
WHERE PlexShow.Key = @Key)",
new { LibraryId = library.Id, show.Key }).Map(count => count > 0);
}
public async Task<Option<int>> FlagUnavailable(PlexLibrary library, PlexEpisode episode)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();

Loading…
Cancel
Save