Browse Source

support external subtitles (#745)

* support external subtitles in local movie libraries

* code cleanup

* simplify subtitle updating

* skip external subtitles from media servers

* fix plex subtitles
pull/746/head
Jason Dove 4 years ago committed by GitHub
parent
commit
3713711864
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 36
      ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs
  3. 41
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  4. 44
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  5. 6
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  6. 4
      ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs
  7. 4
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  8. 108
      ErsatzTV.Core.Tests/Metadata/LocalSubtitlesProviderTests.cs
  9. 2
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  10. 16
      ErsatzTV.Core/Domain/Metadata/Subtitle.cs
  11. 44
      ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs
  12. 46
      ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs
  13. 43
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  14. 2
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  15. 25
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  16. 2
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegStreamSelector.cs
  17. 1
      ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs
  18. 8
      ErsatzTV.Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs
  19. 4
      ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs
  20. 5
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  21. 46
      ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  22. 46
      ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  23. 18
      ErsatzTV.Core/Metadata/LocalFileSystem.cs
  24. 1
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  25. 142
      ErsatzTV.Core/Metadata/LocalSubtitlesProvider.cs
  26. 44
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  27. 43
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  28. 38
      ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs
  29. 38
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  30. 45
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  31. 85
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  32. 3
      ErsatzTV.Infrastructure/Data/Configurations/Metadata/SubtitleConfiguration.cs
  33. 28
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  34. 1
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  35. 11
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  36. 1
      ErsatzTV.Infrastructure/Health/HealthCheckService.cs
  37. 4064
      ErsatzTV.Infrastructure/Migrations/20220420024241_Add_SubtitleSDH.Designer.cs
  38. 26
      ErsatzTV.Infrastructure/Migrations/20220420024241_Add_SubtitleSDH.cs
  39. 4067
      ErsatzTV.Infrastructure/Migrations/20220420132931_Add_SubtitleTitle.Designer.cs
  40. 25
      ErsatzTV.Infrastructure/Migrations/20220420132931_Add_SubtitleTitle.cs
  41. 4067
      ErsatzTV.Infrastructure/Migrations/20220420145640_Reset_AllStatistics20220420.Designer.cs
  42. 23
      ErsatzTV.Infrastructure/Migrations/20220420145640_Reset_AllStatistics20220420.cs
  43. 8
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  44. 6
      ErsatzTV.Infrastructure/Plex/Models/PlexStreamResponse.cs
  45. 2
      ErsatzTV.Infrastructure/Trakt/SnakeCasePropertyNamesContractResolver.cs
  46. 1
      ErsatzTV.sln.DotSettings
  47. 1
      ErsatzTV/Startup.cs

2
CHANGELOG.md

@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Add support for burning in embedded text subtitles
- Add support for burning in external text subtitles
- **This requires a one-time full library scan, which may take a long time with large libraries.**
## [0.5.1-beta] - 2022-04-17
### Fixed

36
ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs

@ -1,45 +1,15 @@ @@ -1,45 +1,15 @@
using System.Globalization;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaItems;
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<CultureInfo>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaItemRepository _mediaItemRepository;
public GetAllLanguageCodesHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaItemRepository mediaItemRepository)
{
_dbContextFactory = dbContextFactory;
public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository;
}
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
var result = new System.Collections.Generic.HashSet<CultureInfo>();
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
List<string> mediaCodes = await _mediaItemRepository.GetAllLanguageCodes();
foreach (string mediaCode in mediaCodes)
{
foreach (string code in await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCode))
{
Option<CultureInfo> maybeCulture = allCultures.Find(
c => string.Equals(code, c.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase));
foreach (CultureInfo culture in maybeCulture)
{
result.Add(culture);
}
}
}
return result.ToList();
}
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken) =>
await _mediaItemRepository.GetAllLanguageCodeCultures();
}

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

@ -151,7 +151,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -151,7 +151,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
List<Subtitle> subtitles = GetSubtitles(playoutItemWithPath.PlayoutItem.MediaItem);
List<Subtitle> subtitles = GetSubtitles(playoutItemWithPath);
Command process = await _ffmpegProcessService.ForPlayoutItem(
ffmpegPath,
@ -236,8 +236,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -236,8 +236,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
return BaseError.New($"Unexpected error locating playout item for channel {channel.Number}");
}
private static List<Subtitle> GetSubtitles(MediaItem mediaItem) =>
mediaItem switch
private static List<Subtitle> GetSubtitles(PlayoutItemWithPath playoutItemWithPath)
{
List<Subtitle> allSubtitles = playoutItemWithPath.PlayoutItem.MediaItem switch
{
Episode episode => episode.EpisodeMetadata.HeadOrNone()
.Map(mm => mm.Subtitles)
@ -254,6 +255,40 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -254,6 +255,40 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
_ => new List<Subtitle>()
};
bool isMediaServer = playoutItemWithPath.PlayoutItem.MediaItem is PlexMovie or PlexEpisode or
JellyfinMovie or JellyfinEpisode or EmbyMovie or EmbyEpisode;
if (isMediaServer)
{
string mediaItemFolder = Path.GetDirectoryName(playoutItemWithPath.Path);
allSubtitles = allSubtitles.Map<Subtitle, Option<Subtitle>>(
subtitle =>
{
if (subtitle.SubtitleKind == SubtitleKind.Sidecar)
{
// need to prepend path with movie/episode folder
if (!string.IsNullOrWhiteSpace(mediaItemFolder))
{
subtitle.Path = Path.Combine(mediaItemFolder, subtitle.Path);
// skip subtitles that don't exist
if (!File.Exists(subtitle.Path))
{
return None;
}
}
}
return subtitle;
})
.Somes()
.ToList();
}
return allSubtitles;
}
private async Task<Either<BaseError, PlayoutItemWithPath>> CheckForFallbackFiller(
TvContext dbContext,
Channel channel,

44
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs

@ -6,7 +6,10 @@ using CliWrap.Builders; @@ -6,7 +6,10 @@ using CliWrap.Builders;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -17,16 +20,25 @@ namespace ErsatzTV.Application.Subtitles; @@ -17,16 +20,25 @@ namespace ErsatzTV.Application.Subtitles;
public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSubtitles, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<ExtractEmbeddedSubtitlesHandler> _logger;
private readonly IPlexPathReplacementService _plexPathReplacementService;
public ExtractEmbeddedSubtitlesHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem,
IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService,
ILogger<ExtractEmbeddedSubtitlesHandler> logger)
{
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_plexPathReplacementService = plexPathReplacementService;
_jellyfinPathReplacementService = jellyfinPathReplacementService;
_embyPathReplacementService = embyPathReplacementService;
_logger = logger;
}
@ -257,7 +269,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -257,7 +269,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
}
}
string mediaItemPath = mediaItem.GetHeadVersion().MediaFiles.Head().Path;
string mediaItemPath = await GetMediaItemPath(mediaItem);
ArgumentsBuilder args = new ArgumentsBuilder()
.Add("-nostdin")
@ -380,6 +392,36 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -380,6 +392,36 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
return Path.Combine(subfolder, subfolder2, nameWithExtension);
}
private async Task<string> GetMediaItemPath(MediaItem mediaItem)
{
MediaVersion version = mediaItem.GetHeadVersion();
MediaFile file = version.MediaFiles.Head();
string path = file.Path;
return mediaItem switch
{
PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath(
plexMovie.LibraryPathId,
path),
PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath(
plexEpisode.LibraryPathId,
path),
JellyfinMovie jellyfinMovie => await _jellyfinPathReplacementService.GetReplacementJellyfinPath(
jellyfinMovie.LibraryPathId,
path),
JellyfinEpisode jellyfinEpisode => await _jellyfinPathReplacementService.GetReplacementJellyfinPath(
jellyfinEpisode.LibraryPathId,
path),
EmbyMovie embyMovie => await _embyPathReplacementService.GetReplacementEmbyPath(
embyMovie.LibraryPathId,
path),
EmbyEpisode embyEpisode => await _embyPathReplacementService.GetReplacementEmbyPath(
embyEpisode.LibraryPathId,
path),
_ => path
};
}
private static string GetStringHash(string text)
{
if (string.IsNullOrEmpty(text))

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

@ -531,8 +531,10 @@ public class TranscodingTests @@ -531,8 +531,10 @@ public class TranscodingTests
public Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version) =>
Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
public Task<Option<MediaStream>> SelectSubtitleStream(Channel channel, MediaVersion version) =>
Optional(version.Streams.Find(s => s.MediaStreamKind == MediaStreamKind.Subtitle)).AsTask();
public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
Channel channel,
MediaVersion version,
List<Domain.Subtitle> subtitles) => subtitles.HeadOrNone().AsTask();
}
private static string ExecutableName(string baseName) =>

4
ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs

@ -45,6 +45,10 @@ public class FakeLocalFileSystem : ILocalFileSystem @@ -45,6 +45,10 @@ public class FakeLocalFileSystem : ILocalFileSystem
public IEnumerable<string> ListFiles(string folder) =>
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
// TODO: this isn't accurate, need to use search pattern
public IEnumerable<string> ListFiles(string folder, string searchPattern) =>
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
public bool FileExists(string path) => _files.Any(f => f.Path == path);
public bool FolderExists(string folder) => false;

4
ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs

@ -67,7 +67,9 @@ public class FakeTelevisionRepository : ITelevisionRepository @@ -67,7 +67,9 @@ public class FakeTelevisionRepository : ITelevisionRepository
public Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) =>
throw new NotSupportedException();
public Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item) =>
public Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> GetOrAddPlexEpisode(
PlexLibrary library,
PlexEpisode item) =>
throw new NotSupportedException();
public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException();

108
ErsatzTV.Core.Tests/Metadata/LocalSubtitlesProviderTests.cs

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
using System.Globalization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Tests.Fakes;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata;
[TestFixture]
public class LocalSubtitlesProviderTests
{
// test cases are from plex's example folder layout here
// https://support.plex.tv/articles/200471133-adding-local-subtitles-to-your-media/
// /Movies
// /Avatar (2009)
// Avatar (2009).mkv
// Avatar (2009).eng.srt
// Avatar (2009).en.forced.ass
// Avatar (2009).en.sdh.srt
// Avatar (2009).de.srt
// Avatar (2009).de.sdh.forced.srt
[Test]
public void Should_Find_All_Languages_Codecs_And_Flags_With_Full_Paths()
{
// normally this will have a full list from the database, but we just need these two for testing
var cultures = new List<CultureInfo>
{
CultureInfo.GetCultureInfo("en-US"),
CultureInfo.GetCultureInfo("de-DE")
};
var fakeFiles = new List<FakeFileEntry>
{
new(@"/Movies/Avatar (2009)/Avatar (2009).mkv"),
new(@"/Movies/Avatar (2009)/Avatar (2009).eng.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).en.forced.ass"),
new(@"/Movies/Avatar (2009)/Avatar (2009).en.sdh.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).de.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).de.sdh.forced.srt")
};
var provider = new LocalSubtitlesProvider(
new Mock<IMediaItemRepository>().Object,
new Mock<IMetadataRepository>().Object,
new FakeLocalFileSystem(fakeFiles),
new Mock<ILogger<LocalSubtitlesProvider>>().Object);
List<Subtitle> result = provider.LocateExternalSubtitles(
cultures,
@"/Movies/Avatar (2009)/Avatar (2009).mkv",
true);
result.Count.Should().Be(5);
result.Count(s => s.Language == "eng").Should().Be(3);
result.Count(s => s.Language == "deu").Should().Be(2);
result.Count(s => s.Forced).Should().Be(2);
result.Count(s => s.SDH).Should().Be(2);
result.Count(s => s.Codec == "subrip").Should().Be(4);
result.Count(s => s.Codec == "ass").Should().Be(1);
result.All(s => s.Path.Contains(@"/Movies/Avatar (2009)/")).Should().BeTrue();
}
[Test]
public void Should_Find_All_Languages_Codecs_And_Flags_With_File_Names()
{
// normally this will have a full list from the database, but we just need these two for testing
var cultures = new List<CultureInfo>
{
CultureInfo.GetCultureInfo("en-US"),
CultureInfo.GetCultureInfo("de-DE")
};
var fakeFiles = new List<FakeFileEntry>
{
new(@"/Movies/Avatar (2009)/Avatar (2009).mkv"),
new(@"/Movies/Avatar (2009)/Avatar (2009).eng.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).en.forced.ass"),
new(@"/Movies/Avatar (2009)/Avatar (2009).en.sdh.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).de.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).de.sdh.forced.srt")
};
var provider = new LocalSubtitlesProvider(
new Mock<IMediaItemRepository>().Object,
new Mock<IMetadataRepository>().Object,
new FakeLocalFileSystem(fakeFiles),
new Mock<ILogger<LocalSubtitlesProvider>>().Object);
List<Subtitle> result = provider.LocateExternalSubtitles(
cultures,
@"/Movies/Avatar (2009)/Avatar (2009).mkv",
false);
result.Count.Should().Be(5);
result.Count(s => s.Language == "eng").Should().Be(3);
result.Count(s => s.Language == "deu").Should().Be(2);
result.Count(s => s.Forced).Should().Be(2);
result.Count(s => s.SDH).Should().Be(2);
result.Count(s => s.Codec == "subrip").Should().Be(4);
result.Count(s => s.Codec == "ass").Should().Be(1);
result.Count(s => s.Path.Contains(@"/Movies/Avatar (2009)/")).Should().Be(0);
}
}

2
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -617,6 +617,7 @@ public class MovieFolderScannerTests @@ -617,6 +617,7 @@ public class MovieFolderScannerTests
new FakeLocalFileSystem(new List<FakeFileEntry>(files)),
_movieRepository.Object,
_localStatisticsProvider.Object,
new Mock<ILocalSubtitlesProvider>().Object,
_localMetadataProvider.Object,
new Mock<IMetadataRepository>().Object,
_imageCache.Object,
@ -636,6 +637,7 @@ public class MovieFolderScannerTests @@ -636,6 +637,7 @@ public class MovieFolderScannerTests
new FakeLocalFileSystem(new List<FakeFileEntry>(), new List<FakeFolderEntry>(folders)),
_movieRepository.Object,
_localStatisticsProvider.Object,
new Mock<ILocalSubtitlesProvider>().Object,
_localMetadataProvider.Object,
new Mock<IMetadataRepository>().Object,
_imageCache.Object,

16
ErsatzTV.Core/Domain/Metadata/Subtitle.cs

@ -4,14 +4,30 @@ public class Subtitle @@ -4,14 +4,30 @@ public class Subtitle
{
public int Id { get; set; }
public SubtitleKind SubtitleKind { get; set; }
public string Title { get; set; }
public int StreamIndex { get; set; }
public string Codec { get; set; }
public bool Default { get; set; }
public bool Forced { get; set; }
public bool SDH { get; set; }
public string Language { get; set; }
public bool IsExtracted { get; set; }
public string Path { get; set; }
public DateTime DateAdded { get; set; }
public DateTime DateUpdated { get; set; }
public bool IsImage => Codec is "hdmv_pgs_subtitle" or "dvd_subtitle";
public static Subtitle FromMediaStream(MediaStream stream) =>
new()
{
Codec = stream.Codec,
Title = stream.Title,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
}

44
ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -16,9 +15,9 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner @@ -16,9 +15,9 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner
private readonly IEmbyApiClient _embyApiClient;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<EmbyMovieLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMetadataRepository _metadataRepository;
private readonly IMediator _mediator;
private readonly IMovieRepository _movieRepository;
private readonly IEmbyPathReplacementService _pathReplacementService;
@ -33,9 +32,9 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner @@ -33,9 +32,9 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner
ISearchRepository searchRepository,
IEmbyPathReplacementService pathReplacementService,
IMediaSourceRepository mediaSourceRepository,
IMetadataRepository metadataRepository,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILogger<EmbyMovieLibraryScanner> logger)
{
_embyApiClient = embyApiClient;
@ -45,9 +44,9 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner @@ -45,9 +44,9 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner
_searchRepository = searchRepository;
_pathReplacementService = pathReplacementService;
_mediaSourceRepository = mediaSourceRepository;
_metadataRepository = metadataRepository;
_localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_logger = logger;
}
@ -185,7 +184,7 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner @@ -185,7 +184,7 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner
if (refreshResult.Map(t => t).IfLeft(false))
{
refreshResult = await UpdateSubtitles(incomingMovie);
refreshResult = await UpdateSubtitles(incomingMovie, localPath);
}
await refreshResult.Match(
@ -238,45 +237,16 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner @@ -238,45 +237,16 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner
_searchIndex.Commit();
return Unit.Default;
}
private async Task<Either<BaseError, bool>> UpdateSubtitles(EmbyMovie movie)
private async Task<Either<BaseError, bool>> UpdateSubtitles(EmbyMovie movie, string localPath)
{
try
{
foreach (MovieMetadata metadata in movie.MovieMetadata)
{
MediaVersion version = movie.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
return await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
return await _localSubtitlesProvider.UpdateSubtitles(movie, localPath, false);
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
return false;
}
}

46
ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -16,11 +15,11 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner @@ -16,11 +15,11 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
private readonly IEmbyApiClient _embyApiClient;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<EmbyTelevisionLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediator _mediator;
private readonly IEmbyPathReplacementService _pathReplacementService;
private readonly IMetadataRepository _metadataRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly IEmbyTelevisionRepository _televisionRepository;
@ -32,9 +31,9 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner @@ -32,9 +31,9 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
ISearchIndex searchIndex,
ISearchRepository searchRepository,
IEmbyPathReplacementService pathReplacementService,
IMetadataRepository metadataRepository,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
IMediator mediator,
ILogger<EmbyTelevisionLibraryScanner> logger)
{
@ -44,9 +43,9 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner @@ -44,9 +43,9 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
_searchIndex = searchIndex;
_searchRepository = searchRepository;
_pathReplacementService = pathReplacementService;
_metadataRepository = metadataRepository;
_localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_mediator = mediator;
_logger = logger;
}
@ -429,10 +428,10 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner @@ -429,10 +428,10 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
ffprobePath,
incomingEpisode,
localPath);
if (refreshResult.Map(t => t).IfLeft(false))
{
refreshResult = await UpdateSubtitles(incomingEpisode);
refreshResult = await UpdateSubtitles(incomingEpisode, localPath);
}
refreshResult.Match(
@ -445,45 +444,16 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner @@ -445,45 +444,16 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
}
}
}
private async Task<Either<BaseError, bool>> UpdateSubtitles(EmbyEpisode episode)
private async Task<Either<BaseError, bool>> UpdateSubtitles(EmbyEpisode episode, string localPath)
{
try
{
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
{
MediaVersion version = episode.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
return await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
return await _localSubtitlesProvider.UpdateSubtitles(episode, localPath, false);
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
return false;
}
}

43
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -57,8 +57,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -57,8 +57,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
Option<MediaStream> maybeSubtitleStream =
await _ffmpegStreamSelector.SelectSubtitleStream(channel, videoVersion);
Option<Subtitle> maybeSubtitle =
await _ffmpegStreamSelector.SelectSubtitleStream(channel, videoVersion, subtitles);
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
channel.StreamingMode,
@ -131,33 +131,28 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -131,33 +131,28 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
return new AudioInputFile(audioPath, new List<AudioStream> { ffmpegAudioStream }, audioState);
});
Option<SubtitleInputFile> subtitleInputFile = maybeSubtitleStream.Map<Option<SubtitleInputFile>>(
subtitleStream =>
Option<SubtitleInputFile> subtitleInputFile = maybeSubtitle.Map<Option<SubtitleInputFile>>(
subtitle =>
{
foreach (Subtitle subtitle in Optional(subtitles.Find(s => s.StreamIndex == subtitleStream.Index)))
if (!subtitle.IsImage && subtitle.SubtitleKind == SubtitleKind.Embedded && !subtitle.IsExtracted)
{
if (!subtitle.IsImage && !subtitle.IsExtracted)
{
_logger.LogWarning("Subtitles are not yet available for this item");
return None;
}
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(
subtitle.IsImage ? subtitleStream.Index : 0,
subtitleStream.Codec,
StreamKind.Video);
_logger.LogWarning("Subtitles are not yet available for this item");
return None;
}
string path = subtitle.IsImage
? videoPath
: Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path);
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(
subtitle.IsImage ? subtitle.StreamIndex : 0,
subtitle.Codec,
StreamKind.Video);
return new SubtitleInputFile(
path,
new List<ErsatzTV.FFmpeg.MediaStream> { ffmpegSubtitleStream },
false);
}
string path = subtitle.IsImage
? videoPath
: Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path);
return None;
return new SubtitleInputFile(
path,
new List<ErsatzTV.FFmpeg.MediaStream> { ffmpegSubtitleStream },
false);
// TODO: figure out HLS direct
// channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect);

2
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -171,7 +171,7 @@ public class FFmpegPlaybackSettingsCalculator @@ -171,7 +171,7 @@ public class FFmpegPlaybackSettingsCalculator
}
public FFmpegPlaybackSettings CalculateErrorSettings(FFmpegProfile ffmpegProfile) =>
new FFmpegPlaybackSettings
new()
{
HardwareAcceleration = HardwareAccelerationKind.None,
ThreadCount = ffmpegProfile.ThreadCount,

25
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -81,7 +81,10 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -81,7 +81,10 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
return audioStreams.OrderByDescending(s => s.Channels).Head();
}
public async Task<Option<MediaStream>> SelectSubtitleStream(Channel channel, MediaVersion version)
public async Task<Option<Subtitle>> SelectSubtitleStream(
Channel channel,
MediaVersion version,
List<Subtitle> subtitles)
{
if (channel.SubtitleMode == ChannelSubtitleMode.None)
{
@ -97,10 +100,6 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -97,10 +100,6 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
return None;
}
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
string language = (channel.PreferredSubtitleLanguageCode ?? string.Empty).ToLowerInvariant();
if (string.IsNullOrWhiteSpace(language))
{
@ -110,34 +109,34 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -110,34 +109,34 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
{
// filter to preferred language
List<string> allCodes = await _searchRepository.GetAllLanguageCodes(new List<string> { language });
subtitleStreams = subtitleStreams
subtitles = subtitles
.Filter(
s => allCodes.Any(c => string.Equals(s.Language, c, StringComparison.InvariantCultureIgnoreCase)))
.ToList();
}
if (subtitleStreams.Count > 0)
if (subtitles.Count > 0)
{
switch (channel.SubtitleMode)
{
case ChannelSubtitleMode.Forced:
foreach (MediaStream stream in subtitleStreams.OrderBy(s => s.Index).Find(s => s.Forced))
foreach (Subtitle subtitle in subtitles.OrderBy(s => s.StreamIndex).Find(s => s.Forced))
{
return stream;
return subtitle;
}
break;
case ChannelSubtitleMode.Default:
foreach (MediaStream stream in subtitleStreams.OrderBy(s => s.Default ? 0 : 1).ThenBy(s => s.Index))
foreach (Subtitle subtitle in subtitles.OrderBy(s => s.Default ? 0 : 1).ThenBy(s => s.StreamIndex))
{
return stream;
return subtitle;
}
break;
case ChannelSubtitleMode.Any:
foreach (MediaStream stream in subtitleStreams.OrderBy(s => s.Index).HeadOrNone())
foreach (Subtitle subtitle in subtitles.OrderBy(s => s.StreamIndex).HeadOrNone())
{
return stream;
return subtitle;
}
break;

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

@ -7,5 +7,5 @@ public interface IFFmpegStreamSelector @@ -7,5 +7,5 @@ public interface IFFmpegStreamSelector
Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version);
Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version);
Task<Option<MediaStream>> SelectSubtitleStream(Channel channel, MediaVersion version);
Task<Option<Subtitle>> SelectSubtitleStream(Channel channel, MediaVersion version, List<Subtitle> subtitles);
}

1
ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs

@ -9,6 +9,7 @@ public interface ILocalFileSystem @@ -9,6 +9,7 @@ public interface ILocalFileSystem
bool IsLibraryPathAccessible(LibraryPath libraryPath);
IEnumerable<string> ListSubdirectories(string folder);
IEnumerable<string> ListFiles(string folder);
IEnumerable<string> ListFiles(string folder, string searchPattern);
bool FileExists(string path);
bool FolderExists(string folder);
Task<Either<BaseError, Unit>> CopyFile(string source, string destination);

8
ErsatzTV.Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Metadata;
public interface ILocalSubtitlesProvider
{
Task<bool> UpdateSubtitles(MediaItem mediaItem, Option<string> localPath, bool saveFullPath);
}

4
ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
using ErsatzTV.Core.Domain;
using System.Globalization;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Repositories;
public interface IMediaItemRepository
{
Task<List<string>> GetAllLanguageCodes();
Task<List<CultureInfo>> GetAllLanguageCodeCultures();
Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path);
Task<Unit> FlagNormal(MediaItem mediaItem);
Task<Either<BaseError, Unit>> DeleteItems(List<int> mediaItemIds);

5
ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs

@ -36,7 +36,10 @@ public interface ITelevisionRepository @@ -36,7 +36,10 @@ public interface ITelevisionRepository
Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath);
Task<Either<BaseError, MediaItemScanResult<PlexShow>>> GetOrAddPlexShow(PlexLibrary library, PlexShow item);
Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item);
Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item);
Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>>
GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item);
Task<bool> AddGenre(ShowMetadata metadata, Genre genre);
Task<bool> AddTag(ShowMetadata metadata, Tag tag);
Task<bool> AddStudio(ShowMetadata metadata, Studio studio);

46
ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -16,9 +15,9 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner @@ -16,9 +15,9 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner
private readonly IJellyfinApiClient _jellyfinApiClient;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<JellyfinMovieLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMetadataRepository _metadataRepository;
private readonly IMediator _mediator;
private readonly IMovieRepository _movieRepository;
private readonly IJellyfinPathReplacementService _pathReplacementService;
@ -33,9 +32,9 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner @@ -33,9 +32,9 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner
ISearchRepository searchRepository,
IJellyfinPathReplacementService pathReplacementService,
IMediaSourceRepository mediaSourceRepository,
IMetadataRepository metadataRepository,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILogger<JellyfinMovieLibraryScanner> logger)
{
_jellyfinApiClient = jellyfinApiClient;
@ -45,9 +44,9 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner @@ -45,9 +44,9 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner
_searchRepository = searchRepository;
_pathReplacementService = pathReplacementService;
_mediaSourceRepository = mediaSourceRepository;
_metadataRepository = metadataRepository;
_localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_logger = logger;
}
@ -182,10 +181,10 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner @@ -182,10 +181,10 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner
ffprobePath,
incomingMovie,
localPath);
if (refreshResult.Map(t => t).IfLeft(false))
{
refreshResult = await UpdateSubtitles(incomingMovie);
refreshResult = await UpdateSubtitles(incomingMovie, localPath);
}
await refreshResult.Match(
@ -238,45 +237,16 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner @@ -238,45 +237,16 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner
_searchIndex.Commit();
return Unit.Default;
}
private async Task<Either<BaseError, bool>> UpdateSubtitles(JellyfinMovie movie)
private async Task<Either<BaseError, bool>> UpdateSubtitles(JellyfinMovie movie, string localPath)
{
try
{
foreach (MovieMetadata metadata in movie.MovieMetadata)
{
MediaVersion version = movie.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
return await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
return await _localSubtitlesProvider.UpdateSubtitles(movie, localPath, false);
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
return false;
}
}

46
ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -16,11 +15,11 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne @@ -16,11 +15,11 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
private readonly IJellyfinApiClient _jellyfinApiClient;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<JellyfinTelevisionLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediator _mediator;
private readonly IJellyfinPathReplacementService _pathReplacementService;
private readonly IMetadataRepository _metadataRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly IJellyfinTelevisionRepository _televisionRepository;
@ -32,9 +31,9 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne @@ -32,9 +31,9 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
ISearchIndex searchIndex,
ISearchRepository searchRepository,
IJellyfinPathReplacementService pathReplacementService,
IMetadataRepository metadataRepository,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
IMediator mediator,
ILogger<JellyfinTelevisionLibraryScanner> logger)
{
@ -44,9 +43,9 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne @@ -44,9 +43,9 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
_searchIndex = searchIndex;
_searchRepository = searchRepository;
_pathReplacementService = pathReplacementService;
_metadataRepository = metadataRepository;
_localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_mediator = mediator;
_logger = logger;
}
@ -431,10 +430,10 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne @@ -431,10 +430,10 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
ffprobePath,
incomingEpisode,
localPath);
if (refreshResult.Map(t => t).IfLeft(false))
{
refreshResult = await UpdateSubtitles(incomingEpisode);
refreshResult = await UpdateSubtitles(incomingEpisode, localPath);
}
refreshResult.Match(
@ -447,45 +446,16 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne @@ -447,45 +446,16 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
}
}
}
private async Task<Either<BaseError, bool>> UpdateSubtitles(JellyfinEpisode episode)
private async Task<Either<BaseError, bool>> UpdateSubtitles(JellyfinEpisode episode, string localPath)
{
try
{
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
{
MediaVersion version = episode.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
return await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
return await _localSubtitlesProvider.UpdateSubtitles(episode, localPath, false);
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
return false;
}
}

18
ErsatzTV.Core/Metadata/LocalFileSystem.cs

@ -75,6 +75,24 @@ public class LocalFileSystem : ILocalFileSystem @@ -75,6 +75,24 @@ public class LocalFileSystem : ILocalFileSystem
return new List<string>();
}
public IEnumerable<string> ListFiles(string folder, string searchPattern)
{
if (Directory.Exists(folder))
{
try
{
return Directory.EnumerateFiles(folder, searchPattern, SearchOption.TopDirectoryOnly);
}
catch (Exception ex)
{
// do nothing
_client.Notify(ex);
}
}
return new List<string>();
}
public bool FileExists(string path) => File.Exists(path);
public bool FolderExists(string folder) => Directory.Exists(folder);

1
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -372,6 +372,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider @@ -372,6 +372,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
if (subtitleStream.tags is not null)
{
stream.Language = subtitleStream.tags.language;
stream.Title = subtitleStream.tags.title;
}
version.Streams.Add(stream);

142
ErsatzTV.Core/Metadata/LocalSubtitlesProvider.cs

@ -0,0 +1,142 @@ @@ -0,0 +1,142 @@
using System.Globalization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Metadata;
public class LocalSubtitlesProvider : ILocalSubtitlesProvider
{
private readonly List<CultureInfo> _languageCodes = new();
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<LocalSubtitlesProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository;
private readonly IMetadataRepository _metadataRepository;
private readonly SemaphoreSlim _slim = new(1, 1);
public LocalSubtitlesProvider(
IMediaItemRepository mediaItemRepository,
IMetadataRepository metadataRepository,
ILocalFileSystem localFileSystem,
ILogger<LocalSubtitlesProvider> logger)
{
_mediaItemRepository = mediaItemRepository;
_metadataRepository = metadataRepository;
_localFileSystem = localFileSystem;
_logger = logger;
}
public async Task<bool> UpdateSubtitles(MediaItem mediaItem, Option<string> localPath, bool saveFullPath)
{
if (_languageCodes.Count == 0)
{
await _slim.WaitAsync();
try
{
_languageCodes.AddRange(await _mediaItemRepository.GetAllLanguageCodeCultures());
}
finally
{
_slim.Release();
}
}
if (_languageCodes.Count == 0)
{
_logger.LogError("Failed to update subtitles; unable to load languages from database");
return false;
}
Option<Domain.Metadata> maybeMetadata = mediaItem switch
{
Episode e => e.EpisodeMetadata.OfType<Domain.Metadata>().HeadOrNone(),
Movie m => m.MovieMetadata.OfType<Domain.Metadata>().HeadOrNone(),
MusicVideo mv => mv.MusicVideoMetadata.OfType<Domain.Metadata>().HeadOrNone(),
OtherVideo ov => ov.OtherVideoMetadata.OfType<Domain.Metadata>().HeadOrNone(),
_ => None
};
foreach (Domain.Metadata metadata in maybeMetadata)
{
MediaVersion version = mediaItem.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = subtitleStreams.Map(Subtitle.FromMediaStream).ToList();
string mediaItemPath = await localPath.IfNoneAsync(() => mediaItem.GetHeadVersion().MediaFiles.Head().Path);
subtitles.AddRange(LocateExternalSubtitles(_languageCodes, mediaItemPath, saveFullPath));
await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
return false;
}
public List<Subtitle> LocateExternalSubtitles(
List<CultureInfo> languageCodes,
string mediaItemPath,
bool saveFullPath)
{
var result = new List<Subtitle>();
string folder = Path.GetDirectoryName(mediaItemPath);
string withoutExtension = Path.GetFileNameWithoutExtension(mediaItemPath);
foreach (string file in _localFileSystem.ListFiles(folder, $"{withoutExtension}*"))
{
string fileName = Path.GetFileName(file);
if (!fileName.StartsWith(withoutExtension))
{
continue;
}
string extension = Path.GetExtension(file);
string codec = extension switch
{
".ssa" or ".ass" => "ass",
".srt" => "subrip",
".vtt" => "webvtt",
_ => string.Empty
};
if (string.IsNullOrWhiteSpace(codec))
{
continue;
}
bool forced = file.Contains(".forced.");
bool sdh = file.Contains(".sdh.") || file.Contains(".cc.");
string language = fileName
.Replace($"{withoutExtension}.", string.Empty)[..3]
.Replace(".", string.Empty);
Option<CultureInfo> maybeCulture = languageCodes.Find(
ci => ci.TwoLetterISOLanguageName == language || ci.ThreeLetterISOLanguageName == language);
foreach (CultureInfo culture in maybeCulture)
{
_logger.LogDebug("Located {Attribute} at {Path}", "External Subtitles", file);
result.Add(
new Subtitle
{
SubtitleKind = SubtitleKind.Sidecar,
Codec = codec,
Default = false,
Forced = forced,
SDH = sdh,
Language = culture.ThreeLetterISOLanguageName,
Path = saveFullPath ? file : Path.GetFileName(file),
DateAdded = DateTime.UtcNow,
DateUpdated = _localFileSystem.GetLastWriteTime(file)
});
}
}
return result;
}
}

44
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -18,9 +17,9 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -18,9 +17,9 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<MovieFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
private readonly IMovieRepository _movieRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
@ -29,6 +28,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -29,6 +28,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
ILocalFileSystem localFileSystem,
IMovieRepository movieRepository,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILocalMetadataProvider localMetadataProvider,
IMetadataRepository metadataRepository,
IImageCache imageCache,
@ -54,8 +54,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -54,8 +54,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
{
_localFileSystem = localFileSystem;
_movieRepository = movieRepository;
_localSubtitlesProvider = localSubtitlesProvider;
_localMetadataProvider = localMetadataProvider;
_metadataRepository = metadataRepository;
_searchIndex = searchIndex;
_searchRepository = searchRepository;
_libraryRepository = libraryRepository;
@ -139,7 +139,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -139,7 +139,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
.BindT(UpdateMetadata)
.BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster, cancellationToken))
.BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt, cancellationToken))
.BindT(movie => UpdateSubtitles(movie, cancellationToken))
.BindT(UpdateSubtitles)
.BindT(FlagNormal);
await maybeMovie.Match(
@ -255,43 +255,11 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -255,43 +255,11 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
}
}
private async Task<Either<BaseError, MediaItemScanResult<Movie>>> UpdateSubtitles(
MediaItemScanResult<Movie> result,
CancellationToken _)
private async Task<Either<BaseError, MediaItemScanResult<Movie>>> UpdateSubtitles(MediaItemScanResult<Movie> result)
{
try
{
Movie movie = result.Item;
foreach (MovieMetadata metadata in movie.MovieMetadata)
{
MediaVersion version = movie.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
await _localSubtitlesProvider.UpdateSubtitles(result.Item, None, true);
return result;
}
catch (Exception ex)

43
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -18,9 +17,9 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -18,9 +17,9 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<MusicVideoFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
private readonly IMusicVideoRepository _musicVideoRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
@ -29,6 +28,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -29,6 +28,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
IMetadataRepository metadataRepository,
IImageCache imageCache,
ISearchIndex searchIndex,
@ -54,7 +54,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -54,7 +54,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{
_localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider;
_metadataRepository = metadataRepository;
_localSubtitlesProvider = localSubtitlesProvider;
_searchIndex = searchIndex;
_searchRepository = searchRepository;
_artistRepository = artistRepository;
@ -290,7 +290,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -290,7 +290,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
.BindT(musicVideo => UpdateStatistics(musicVideo, ffmpegPath, ffprobePath))
.BindT(UpdateMetadata)
.BindT(result => UpdateThumbnail(result, cancellationToken))
.BindT(result => UpdateSubtitles(result, cancellationToken))
.BindT(UpdateSubtitles)
.BindT(FlagNormal);
await maybeMusicVideo.Match(
@ -415,42 +415,11 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -415,42 +415,11 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
}
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateSubtitles(
MediaItemScanResult<MusicVideo> result,
CancellationToken _)
MediaItemScanResult<MusicVideo> result)
{
try
{
MusicVideo musicVideo = result.Item;
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata)
{
MediaVersion version = musicVideo.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
await _localSubtitlesProvider.UpdateSubtitles(result.Item, None, true);
return result;
}
catch (Exception ex)

38
ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -17,9 +16,9 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -17,9 +16,9 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<OtherVideoFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
private readonly IOtherVideoRepository _otherVideoRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
@ -28,6 +27,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -28,6 +27,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
IMetadataRepository metadataRepository,
IImageCache imageCache,
IMediator mediator,
@ -52,7 +52,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -52,7 +52,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
{
_localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider;
_metadataRepository = metadataRepository;
_localSubtitlesProvider = localSubtitlesProvider;
_mediator = mediator;
_searchIndex = searchIndex;
_searchRepository = searchRepository;
@ -210,37 +210,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -210,37 +210,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
{
try
{
OtherVideo otherVideo = result.Item;
foreach (OtherVideoMetadata metadata in otherVideo.OtherVideoMetadata)
{
MediaVersion version = otherVideo.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
await _localSubtitlesProvider.UpdateSubtitles(result.Item, None, true);
return result;
}
catch (Exception ex)

38
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -17,6 +16,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -17,6 +16,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<TelevisionFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
@ -29,6 +29,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -29,6 +29,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
ITelevisionRepository televisionRepository,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
IMetadataRepository metadataRepository,
IImageCache imageCache,
ISearchIndex searchIndex,
@ -53,6 +54,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -53,6 +54,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
_localFileSystem = localFileSystem;
_televisionRepository = televisionRepository;
_localMetadataProvider = localMetadataProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_metadataRepository = metadataRepository;
_searchIndex = searchIndex;
_searchRepository = searchRepository;
@ -243,7 +245,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -243,7 +245,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
.MapT(_ => episode))
.BindT(UpdateMetadata)
.BindT(e => UpdateThumbnail(e, cancellationToken))
.BindT(e => UpdateSubtitles(e, cancellationToken))
.BindT(UpdateSubtitles)
.BindT(e => FlagNormal(new MediaItemScanResult<Episode>(e)))
.MapT(r => r.Item);
@ -447,39 +449,11 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -447,39 +449,11 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
}
}
private async Task<Either<BaseError, Episode>> UpdateSubtitles(Episode episode, CancellationToken _)
private async Task<Either<BaseError, Episode>> UpdateSubtitles(Episode episode)
{
try
{
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
{
MediaVersion version = episode.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
await _metadataRepository.UpdateSubtitles(metadata, subtitles);
}
await _localSubtitlesProvider.UpdateSubtitles(episode, None, true);
return episode;
}
catch (Exception ex)

45
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
@ -14,6 +13,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan @@ -14,6 +13,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
{
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<PlexMovieLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediator _mediator;
@ -35,6 +35,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan @@ -35,6 +35,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
IPlexPathReplacementService plexPathReplacementService,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILogger<PlexMovieLibraryScanner> logger)
: base(metadataRepository, logger)
{
@ -48,6 +49,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan @@ -48,6 +49,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
_plexPathReplacementService = plexPathReplacementService;
_localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_logger = logger;
}
@ -98,7 +100,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan @@ -98,7 +100,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
.BindT(
existing => UpdateStatistics(pathReplacements, existing, incoming, ffmpegPath, ffprobePath))
.BindT(existing => UpdateMetadata(existing, incoming, library, connection, token))
.BindT(UpdateSubtitles)
.BindT(existing => UpdateSubtitles(pathReplacements, existing, incoming))
.BindT(existing => UpdateArtwork(existing, incoming));
await maybeMovie.Match(
@ -409,39 +411,24 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan @@ -409,39 +411,24 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
}
private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateSubtitles(
MediaItemScanResult<PlexMovie> result)
List<PlexPathReplacement> pathReplacements,
MediaItemScanResult<PlexMovie> result,
PlexMovie incoming)
{
try
{
Movie movie = result.Item;
PlexMovie existing = result.Item;
MediaVersion existingVersion = existing.MediaVersions.Head();
MediaVersion incomingVersion = incoming.MediaVersions.Head();
foreach (MovieMetadata metadata in movie.MovieMetadata)
if (result.IsAdded || incomingVersion.DateUpdated > existingVersion.DateUpdated)
{
MediaVersion version = movie.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
string localPath = _plexPathReplacementService.GetReplacementPlexPath(
pathReplacements,
incoming.MediaVersions.Head().MediaFiles.Head().Path,
false);
await _metadataRepository.UpdateSubtitles(metadata, subtitles);
await _localSubtitlesProvider.UpdateSubtitles(result.Item, localPath, false);
}
return result;

85
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
@ -15,6 +14,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -15,6 +14,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
{
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<PlexTelevisionLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediator _mediator;
@ -36,6 +36,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -36,6 +36,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
IPlexPathReplacementService plexPathReplacementService,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILogger<PlexTelevisionLibraryScanner> logger)
: base(metadataRepository, logger)
{
@ -49,6 +50,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -49,6 +50,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
_plexPathReplacementService = plexPathReplacementService;
_localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_logger = logger;
}
@ -432,7 +434,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -432,7 +434,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
incoming.SeasonId = season.Id;
// TODO: figure out how to rebuild playlists
Either<BaseError, PlexEpisode> maybeEpisode = await _televisionRepository
Either<BaseError, MediaItemScanResult<PlexEpisode>> maybeEpisode = await _televisionRepository
.GetOrAddPlexEpisode(library, incoming)
.BindT(existing => UpdateMetadata(existing, incoming))
.BindT(
@ -445,13 +447,20 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -445,13 +447,20 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
token,
ffmpegPath,
ffprobePath))
.BindT(UpdateSubtitles)
.BindT(existing => UpdateSubtitles(pathReplacements, existing, incoming))
.BindT(existing => UpdateArtwork(existing, incoming));
await maybeEpisode.Match(
async episode =>
async result =>
{
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { episode });
if (result.IsAdded)
{
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { result.Item });
}
else
{
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { result.Item });
}
},
error =>
{
@ -481,8 +490,12 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -481,8 +490,12 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
});
}
private async Task<Either<BaseError, PlexEpisode>> UpdateMetadata(PlexEpisode existing, PlexEpisode incoming)
private async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> UpdateMetadata(
MediaItemScanResult<PlexEpisode> result,
PlexEpisode incoming)
{
PlexEpisode existing = result.Item;
var toUpdate = existing.EpisodeMetadata
.Where(em => incoming.EpisodeMetadata.Any(em2 => em2.EpisodeNumber == em.EpisodeNumber))
.ToList();
@ -507,12 +520,12 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -507,12 +520,12 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
// TODO: update existing metadata
return existing;
return result;
}
private async Task<Either<BaseError, PlexEpisode>> UpdateStatistics(
private async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> UpdateStatistics(
List<PlexPathReplacement> pathReplacements,
PlexEpisode existing,
MediaItemScanResult<PlexEpisode> result,
PlexEpisode incoming,
PlexLibrary library,
PlexConnection connection,
@ -520,6 +533,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -520,6 +533,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
string ffmpegPath,
string ffprobePath)
{
PlexEpisode existing = result.Item;
MediaVersion existingVersion = existing.MediaVersions.Head();
MediaVersion incomingVersion = incoming.MediaVersions.Head();
@ -615,43 +629,31 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -615,43 +629,31 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
});
}
return Right<BaseError, PlexEpisode>(existing);
return result;
}
private async Task<Either<BaseError, PlexEpisode>> UpdateSubtitles(PlexEpisode episode)
private async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> UpdateSubtitles(
List<PlexPathReplacement> pathReplacements,
MediaItemScanResult<PlexEpisode> result,
PlexEpisode incoming)
{
try
{
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
{
MediaVersion version = episode.GetHeadVersion();
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
PlexEpisode existing = result.Item;
MediaVersion existingVersion = existing.MediaVersions.Head();
MediaVersion incomingVersion = incoming.MediaVersions.Head();
var subtitles = new List<Subtitle>();
foreach (MediaStream stream in subtitleStreams)
{
var subtitle = new Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
subtitles.Add(subtitle);
}
if (result.IsAdded || incomingVersion.DateUpdated > existingVersion.DateUpdated)
{
string localPath = _plexPathReplacementService.GetReplacementPlexPath(
pathReplacements,
incoming.MediaVersions.Head().MediaFiles.Head().Path,
false);
await _metadataRepository.UpdateSubtitles(metadata, subtitles);
await _localSubtitlesProvider.UpdateSubtitles(existing, localPath, false);
}
return episode;
return result;
}
catch (Exception ex)
{
@ -659,8 +661,11 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -659,8 +661,11 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
}
}
private async Task<Either<BaseError, PlexEpisode>> UpdateArtwork(PlexEpisode existing, PlexEpisode incoming)
private async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> UpdateArtwork(
MediaItemScanResult<PlexEpisode> result,
PlexEpisode incoming)
{
PlexEpisode existing = result.Item;
foreach (EpisodeMetadata incomingMetadata in incoming.EpisodeMetadata)
{
Option<EpisodeMetadata> maybeExistingMetadata = existing.EpisodeMetadata
@ -676,6 +681,6 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL @@ -676,6 +681,6 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
}
}
return existing;
return result;
}
}

3
ErsatzTV.Infrastructure/Data/Configurations/Metadata/SubtitleConfiguration.cs

@ -12,5 +12,8 @@ public class SubtitleConfiguration : IEntityTypeConfiguration<Subtitle> @@ -12,5 +12,8 @@ public class SubtitleConfiguration : IEntityTypeConfiguration<Subtitle>
builder.Property(s => s.IsExtracted)
.HasDefaultValue(false);
builder.Property(s => s.SDH)
.HasDefaultValue(false);
}
}

28
ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs

@ -1,7 +1,9 @@ @@ -1,7 +1,9 @@
using Dapper;
using System.Globalization;
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories;
@ -26,6 +28,30 @@ public class MediaItemRepository : IMediaItemRepository @@ -26,6 +28,30 @@ public class MediaItemRepository : IMediaItemRepository
.Map(result => result.ToList());
}
public async Task<List<CultureInfo>> GetAllLanguageCodeCultures()
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
var result = new System.Collections.Generic.HashSet<CultureInfo>();
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
List<string> mediaCodes = await GetAllLanguageCodes();
foreach (string mediaCode in mediaCodes)
{
foreach (string code in await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCode))
{
Option<CultureInfo> maybeCulture = allCultures.Find(
c => string.Equals(code, c.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase));
foreach (CultureInfo culture in maybeCulture)
{
result.Add(culture);
}
}
}
return result.ToList();
}
public async Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();

1
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -528,6 +528,7 @@ public class MetadataRepository : IMetadataRepository @@ -528,6 +528,7 @@ public class MetadataRepository : IMetadataRepository
existingSubtitle.Codec = incomingSubtitle.Codec;
existingSubtitle.Default = incomingSubtitle.Default;
existingSubtitle.Forced = incomingSubtitle.Forced;
existingSubtitle.SDH = incomingSubtitle.SDH;
existingSubtitle.Language = incomingSubtitle.Language;
existingSubtitle.SubtitleKind = incomingSubtitle.SubtitleKind;
existingSubtitle.DateUpdated = incomingSubtitle.DateUpdated;

11
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -490,7 +490,9 @@ public class TelevisionRepository : ITelevisionRepository @@ -490,7 +490,9 @@ public class TelevisionRepository : ITelevisionRepository
async () => await AddPlexSeason(dbContext, library, item));
}
public async Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item)
public async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> GetOrAddPlexEpisode(
PlexLibrary library,
PlexEpisode item)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<PlexEpisode> maybeExisting = await dbContext.PlexEpisodes
@ -525,7 +527,8 @@ public class TelevisionRepository : ITelevisionRepository @@ -525,7 +527,8 @@ public class TelevisionRepository : ITelevisionRepository
.SingleOrDefaultAsync(i => i.Key == item.Key);
return await maybeExisting.Match(
plexEpisode => Right<BaseError, PlexEpisode>(plexEpisode).AsTask(),
plexEpisode => Right<BaseError, MediaItemScanResult<PlexEpisode>>(
new MediaItemScanResult<PlexEpisode>(plexEpisode) { IsAdded = false }).AsTask(),
async () => await AddPlexEpisode(dbContext, library, item));
}
@ -872,7 +875,7 @@ public class TelevisionRepository : ITelevisionRepository @@ -872,7 +875,7 @@ public class TelevisionRepository : ITelevisionRepository
}
}
private static async Task<Either<BaseError, PlexEpisode>> AddPlexEpisode(
private static async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> AddPlexEpisode(
TvContext dbContext,
PlexLibrary library,
PlexEpisode item)
@ -900,7 +903,7 @@ public class TelevisionRepository : ITelevisionRepository @@ -900,7 +903,7 @@ public class TelevisionRepository : ITelevisionRepository
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(item).Reference(e => e.Season).LoadAsync();
return item;
return new MediaItemScanResult<PlexEpisode>(item) { IsAdded = true };
}
catch (Exception ex)
{

1
ErsatzTV.Infrastructure/Health/HealthCheckService.cs

@ -6,6 +6,7 @@ namespace ErsatzTV.Infrastructure.Health; @@ -6,6 +6,7 @@ namespace ErsatzTV.Infrastructure.Health;
public class HealthCheckService : IHealthCheckService
{
private readonly List<IHealthCheck> _checks; // ReSharper disable SuggestBaseTypeForParameterInConstructor
public HealthCheckService(
IFFmpegVersionHealthCheck ffmpegVersionHealthCheck,
IFFmpegReportsHealthCheck ffmpegReportsHealthCheck,

4064
ErsatzTV.Infrastructure/Migrations/20220420024241_Add_SubtitleSDH.Designer.cs generated

File diff suppressed because it is too large Load Diff

26
ErsatzTV.Infrastructure/Migrations/20220420024241_Add_SubtitleSDH.cs

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

4067
ErsatzTV.Infrastructure/Migrations/20220420132931_Add_SubtitleTitle.Designer.cs generated

File diff suppressed because it is too large Load Diff

25
ErsatzTV.Infrastructure/Migrations/20220420132931_Add_SubtitleTitle.cs

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_SubtitleTitle : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Title",
table: "Subtitle",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Title",
table: "Subtitle");
}
}
}

4067
ErsatzTV.Infrastructure/Migrations/20220420145640_Reset_AllStatistics20220420.Designer.cs generated

File diff suppressed because it is too large Load Diff

23
ErsatzTV.Infrastructure/Migrations/20220420145640_Reset_AllStatistics20220420.cs

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Reset_AllStatistics20220420 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// reset all local libraries
migrationBuilder.Sql("UPDATE MediaVersion SET DateUpdated = '0001-01-01 00:00:00' WHERE SongId IS NULL");
migrationBuilder.Sql("UPDATE LibraryFolder SET Etag = NULL WHERE Id IN (SELECT LF.Id FROM LibraryFolder LF INNER JOIN LibraryPath LP on LP.Id = LF.LibraryPathId INNER JOIN Library L on L.Id = LP.LibraryId WHERE MediaKind != 5)");
migrationBuilder.Sql("UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00' WHERE Id IN (SELECT LP.Id FROM LibraryPath LP INNER JOIN Library L on L.Id = LP.LibraryId WHERE MediaKind != 5)");
migrationBuilder.Sql("UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE MediaKind != 5");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

8
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -1859,6 +1859,11 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1859,6 +1859,11 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<bool>("SDH")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false);
b.Property<int?>("SeasonMetadataId")
.HasColumnType("INTEGER");
@ -1874,6 +1879,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1874,6 +1879,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("SubtitleKind")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ArtistMetadataId");

6
ErsatzTV.Infrastructure/Plex/Models/PlexStreamResponse.cs

@ -39,4 +39,10 @@ public class PlexStreamResponse @@ -39,4 +39,10 @@ public class PlexStreamResponse
[XmlAttribute("scanType")]
public string ScanType { get; set; }
[XmlAttribute("displayTitle")]
public string DisplayTitle { get; set; }
[XmlAttribute("extendedDisplayTitle")]
public string ExtendedDisplayTitle { get; set; }
}

2
ErsatzTV.Infrastructure/Trakt/SnakeCasePropertyNamesContractResolver.cs

@ -16,7 +16,7 @@ public class DeliminatorSeparatedPropertyNamesContractResolver : DefaultContract @@ -16,7 +16,7 @@ public class DeliminatorSeparatedPropertyNamesContractResolver : DefaultContract
var parts = new List<string>();
var currentWord = new StringBuilder();
foreach (char c in propertyName.ToCharArray())
foreach (char c in propertyName)
{
if (char.IsUpper(c) && currentWord.Length > 0)
{

1
ErsatzTV.sln.DotSettings

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LE/@EntryIndexedValue">LE</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NV/@EntryIndexedValue">NV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SAR/@EntryIndexedValue">SAR</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SDH/@EntryIndexedValue">SDH</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=YUV/@EntryIndexedValue">YUV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=YUVJ/@EntryIndexedValue">YUVJ</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=anull/@EntryIndexedValue">True</s:Boolean>

1
ErsatzTV/Startup.cs

@ -360,6 +360,7 @@ public class Startup @@ -360,6 +360,7 @@ public class Startup
services.AddScoped<ILocalMetadataProvider, LocalMetadataProvider>();
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>();
services.AddScoped<ILocalSubtitlesProvider, LocalSubtitlesProvider>();
services.AddScoped<IPlayoutBuilder, PlayoutBuilder>();
services.AddScoped<IImageCache, ImageCache>();
services.AddScoped<ILocalFileSystem, LocalFileSystem>();

Loading…
Cancel
Save