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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+38
-3
@@ -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<
|
||||
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<
|
||||
_ => 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,
|
||||
|
||||
@@ -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;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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))
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
_searchRepository = searchRepository;
|
||||
_pathReplacementService = pathReplacementService;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_metadataRepository = metadataRepository;
|
||||
_localFileSystem = localFileSystem;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_localSubtitlesProvider = localSubtitlesProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
_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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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);
|
||||
|
||||
string path = subtitle.IsImage
|
||||
? videoPath
|
||||
: Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path);
|
||||
|
||||
return new SubtitleInputFile(
|
||||
path,
|
||||
new List<ErsatzTV.FFmpeg.MediaStream> { ffmpegSubtitleStream },
|
||||
false);
|
||||
_logger.LogWarning("Subtitles are not yet available for this item");
|
||||
return None;
|
||||
}
|
||||
|
||||
return None;
|
||||
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(
|
||||
subtitle.IsImage ? subtitle.StreamIndex : 0,
|
||||
subtitle.Codec,
|
||||
StreamKind.Video);
|
||||
|
||||
string path = subtitle.IsImage
|
||||
? videoPath
|
||||
: Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path);
|
||||
|
||||
return new SubtitleInputFile(
|
||||
path,
|
||||
new List<ErsatzTV.FFmpeg.MediaStream> { ffmpegSubtitleStream },
|
||||
false);
|
||||
|
||||
// TODO: figure out HLS direct
|
||||
// channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect);
|
||||
|
||||
@@ -171,7 +171,7 @@ public class FFmpegPlaybackSettingsCalculator
|
||||
}
|
||||
|
||||
public FFmpegPlaybackSettings CalculateErrorSettings(FFmpegProfile ffmpegProfile) =>
|
||||
new FFmpegPlaybackSettings
|
||||
new()
|
||||
{
|
||||
HardwareAcceleration = HardwareAccelerationKind.None,
|
||||
ThreadCount = ffmpegProfile.ThreadCount,
|
||||
|
||||
@@ -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
|
||||
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
|
||||
{
|
||||
// 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
_searchRepository = searchRepository;
|
||||
_pathReplacementService = pathReplacementService;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_metadataRepository = metadataRepository;
|
||||
_localFileSystem = localFileSystem;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_localSubtitlesProvider = localSubtitlesProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
_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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
_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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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
|
||||
ILocalFileSystem localFileSystem,
|
||||
IMovieRepository movieRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
IMetadataRepository metadataRepository,
|
||||
IImageCache imageCache,
|
||||
@@ -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
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
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
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
IMetadataRepository metadataRepository,
|
||||
IImageCache imageCache,
|
||||
ISearchIndex searchIndex,
|
||||
@@ -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
|
||||
.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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
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
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
IMetadataRepository metadataRepository,
|
||||
IImageCache imageCache,
|
||||
IMediator mediator,
|
||||
@@ -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
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
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
|
||||
ITelevisionRepository televisionRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
IMetadataRepository metadataRepository,
|
||||
IImageCache imageCache,
|
||||
ISearchIndex searchIndex,
|
||||
@@ -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
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
ILogger<PlexMovieLibraryScanner> logger)
|
||||
: base(metadataRepository, logger)
|
||||
{
|
||||
@@ -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
|
||||
.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
|
||||
}
|
||||
|
||||
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();
|
||||
string localPath = _plexPathReplacementService.GetReplacementPlexPath(
|
||||
pathReplacements,
|
||||
incoming.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
false);
|
||||
|
||||
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, localPath, false);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||
ILogger<PlexTelevisionLibraryScanner> logger)
|
||||
: base(metadataRepository, logger)
|
||||
{
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
PlexEpisode existing = result.Item;
|
||||
MediaVersion existingVersion = existing.MediaVersions.Head();
|
||||
MediaVersion incomingVersion = incoming.MediaVersions.Head();
|
||||
|
||||
if (result.IsAdded || incomingVersion.DateUpdated > existingVersion.DateUpdated)
|
||||
{
|
||||
MediaVersion version = episode.GetHeadVersion();
|
||||
var subtitleStreams = version.Streams
|
||||
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
|
||||
.ToList();
|
||||
string localPath = _plexPathReplacementService.GetReplacementPlexPath(
|
||||
pathReplacements,
|
||||
incoming.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
false);
|
||||
|
||||
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(existing, localPath, false);
|
||||
}
|
||||
|
||||
return episode;
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
return existing;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@ public class SubtitleConfiguration : IEntityTypeConfiguration<Subtitle>
|
||||
|
||||
builder.Property(s => s.IsExtracted)
|
||||
.HasDefaultValue(false);
|
||||
|
||||
builder.Property(s => s.SDH)
|
||||
.HasDefaultValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
.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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
b.Property<int>("SubtitleKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ArtistMetadataId");
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user