mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* refactor plex television scanner * refactor emby television scanner * refactor jellyfin television scanner * update changelogpull/769/head
19 changed files with 6802 additions and 2795 deletions
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Metadata; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories; |
||||
|
||||
public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, TEpisode, TEtag> where TLibrary : Library |
||||
where TShow : Show |
||||
where TSeason : Season |
||||
where TEpisode : Episode |
||||
where TEtag : MediaServerItemEtag |
||||
{ |
||||
Task<List<TEtag>> GetExistingShows(TLibrary library); |
||||
Task<List<TEtag>> GetExistingSeasons(TLibrary library, TShow show); |
||||
Task<List<TEtag>> GetExistingEpisodes(TLibrary library, TSeason season); |
||||
Task<Either<BaseError, MediaItemScanResult<TShow>>> GetOrAdd(TLibrary library, TShow item); |
||||
Task<Either<BaseError, MediaItemScanResult<TSeason>>> GetOrAdd(TLibrary library, TSeason item); |
||||
Task<Either<BaseError, MediaItemScanResult<TEpisode>>> GetOrAdd(TLibrary library, TEpisode item); |
||||
Task<Unit> SetEtag(TShow show, string etag); |
||||
Task<Unit> SetEtag(TSeason season, string etag); |
||||
Task<Unit> SetEtag(TEpisode episode, string etag); |
||||
Task<bool> FlagNormal(TLibrary library, TEpisode episode); |
||||
Task<List<int>> FlagFileNotFoundShows(TLibrary library, List<string> showItemIds); |
||||
Task<List<int>> FlagFileNotFoundSeasons(TLibrary library, List<string> seasonItemIds); |
||||
Task<List<int>> FlagFileNotFoundEpisodes(TLibrary library, List<string> episodeItemIds); |
||||
Task<Option<int>> FlagUnavailable(TLibrary library, TEpisode episode); |
||||
} |
@ -0,0 +1,617 @@
@@ -0,0 +1,617 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.MediaServer; |
||||
using ErsatzTV.Core.Errors; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Search; |
||||
using MediatR; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Core.Metadata; |
||||
|
||||
public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, TLibrary, TShow, TSeason, TEpisode, |
||||
TEtag> |
||||
where TConnectionParameters : MediaServerConnectionParameters |
||||
where TLibrary : Library |
||||
where TShow : Show |
||||
where TSeason : Season |
||||
where TEpisode : Episode |
||||
where TEtag : MediaServerItemEtag |
||||
{ |
||||
private readonly ILocalFileSystem _localFileSystem; |
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider; |
||||
private readonly ILocalSubtitlesProvider _localSubtitlesProvider; |
||||
private readonly ILogger _logger; |
||||
private readonly IMediator _mediator; |
||||
private readonly ISearchIndex _searchIndex; |
||||
private readonly ISearchRepository _searchRepository; |
||||
|
||||
protected MediaServerTelevisionLibraryScanner( |
||||
ILocalStatisticsProvider localStatisticsProvider, |
||||
ILocalSubtitlesProvider localSubtitlesProvider, |
||||
ILocalFileSystem localFileSystem, |
||||
ISearchRepository searchRepository, |
||||
ISearchIndex searchIndex, |
||||
IMediator mediator, |
||||
ILogger logger) |
||||
{ |
||||
_localStatisticsProvider = localStatisticsProvider; |
||||
_localSubtitlesProvider = localSubtitlesProvider; |
||||
_localFileSystem = localFileSystem; |
||||
_searchRepository = searchRepository; |
||||
_searchIndex = searchIndex; |
||||
_mediator = mediator; |
||||
_logger = logger; |
||||
} |
||||
|
||||
protected async Task<Either<BaseError, Unit>> ScanLibrary( |
||||
IMediaServerTelevisionRepository<TLibrary, TShow, TSeason, TEpisode, TEtag> televisionRepository, |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
Func<TEpisode, string> getLocalPath, |
||||
string ffmpegPath, |
||||
string ffprobePath, |
||||
bool deepScan, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
try |
||||
{ |
||||
Either<BaseError, List<TShow>> entries = await GetShowLibraryItems(connectionParameters, library); |
||||
|
||||
foreach (BaseError error in entries.LeftToSeq()) |
||||
{ |
||||
return error; |
||||
} |
||||
|
||||
return await ScanLibrary( |
||||
televisionRepository, |
||||
connectionParameters, |
||||
library, |
||||
getLocalPath, |
||||
ffmpegPath, |
||||
ffprobePath, |
||||
entries.RightToSeq().Flatten().ToList(), |
||||
deepScan, |
||||
cancellationToken); |
||||
} |
||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) |
||||
{ |
||||
return new ScanCanceled(); |
||||
} |
||||
finally |
||||
{ |
||||
_searchIndex.Commit(); |
||||
} |
||||
} |
||||
|
||||
protected abstract Task<Either<BaseError, List<TShow>>> GetShowLibraryItems( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library); |
||||
|
||||
protected abstract string MediaServerItemId(TShow show); |
||||
protected abstract string MediaServerItemId(TSeason season); |
||||
protected abstract string MediaServerItemId(TEpisode episode); |
||||
protected abstract string MediaServerEtag(TShow show); |
||||
protected abstract string MediaServerEtag(TSeason season); |
||||
protected abstract string MediaServerEtag(TEpisode episode); |
||||
|
||||
private async Task<Either<BaseError, Unit>> ScanLibrary( |
||||
IMediaServerTelevisionRepository<TLibrary, TShow, TSeason, TEpisode, TEtag> televisionRepository, |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
Func<TEpisode, string> getLocalPath, |
||||
string ffmpegPath, |
||||
string ffprobePath, |
||||
List<TShow> showEntries, |
||||
bool deepScan, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
List<TEtag> existingShows = await televisionRepository.GetExistingShows(library); |
||||
|
||||
var sortedShows = showEntries.OrderBy(s => s.ShowMetadata.Head().SortTitle).ToList(); |
||||
foreach (TShow incoming in showEntries) |
||||
{ |
||||
if (cancellationToken.IsCancellationRequested) |
||||
{ |
||||
return new ScanCanceled(); |
||||
} |
||||
|
||||
decimal percentCompletion = (decimal)sortedShows.IndexOf(incoming) / sortedShows.Count; |
||||
await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion), cancellationToken); |
||||
|
||||
Either<BaseError, MediaItemScanResult<TShow>> maybeShow = await televisionRepository |
||||
.GetOrAdd(library, incoming) |
||||
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan)); |
||||
|
||||
if (maybeShow.IsLeft) |
||||
{ |
||||
foreach (BaseError error in maybeShow.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning( |
||||
"Error processing show {Title}: {Error}", |
||||
incoming.ShowMetadata.Head().Title, |
||||
error.Value); |
||||
} |
||||
|
||||
continue; |
||||
} |
||||
|
||||
foreach (MediaItemScanResult<TShow> result in maybeShow.RightToSeq()) |
||||
{ |
||||
Either<BaseError, List<TSeason>> entries = await GetSeasonLibraryItems( |
||||
library, |
||||
connectionParameters, |
||||
result.Item); |
||||
|
||||
foreach (BaseError error in entries.LeftToSeq()) |
||||
{ |
||||
return error; |
||||
} |
||||
|
||||
Either<BaseError, Unit> scanResult = await ScanSeasons( |
||||
televisionRepository, |
||||
library, |
||||
getLocalPath, |
||||
result.Item, |
||||
connectionParameters, |
||||
ffmpegPath, |
||||
ffprobePath, |
||||
entries.RightToSeq().Flatten().ToList(), |
||||
deepScan, |
||||
cancellationToken); |
||||
|
||||
foreach (ScanCanceled error in scanResult.LeftToSeq().OfType<ScanCanceled>()) |
||||
{ |
||||
return error; |
||||
} |
||||
|
||||
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming)); |
||||
|
||||
if (result.IsAdded || result.IsUpdated) |
||||
{ |
||||
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// trash shows that are no longer present on the media server
|
||||
var fileNotFoundItemIds = existingShows.Map(s => s.MediaServerItemId) |
||||
.Except(showEntries.Map(MediaServerItemId)).ToList(); |
||||
List<int> ids = await televisionRepository.FlagFileNotFoundShows(library, fileNotFoundItemIds); |
||||
await _searchIndex.RebuildItems(_searchRepository, ids); |
||||
|
||||
await _mediator.Publish(new LibraryScanProgress(library.Id, 0), cancellationToken); |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
protected abstract Task<Either<BaseError, List<TSeason>>> GetSeasonLibraryItems( |
||||
TLibrary library, |
||||
TConnectionParameters connectionParameters, |
||||
TShow show); |
||||
|
||||
protected abstract Task<Either<BaseError, List<TEpisode>>> GetEpisodeLibraryItems( |
||||
TLibrary library, |
||||
TConnectionParameters connectionParameters, |
||||
TSeason season); |
||||
|
||||
protected abstract Task<Option<ShowMetadata>> GetFullMetadata( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TShow> result, |
||||
TShow incoming, |
||||
bool deepScan); |
||||
|
||||
protected abstract Task<Option<SeasonMetadata>> GetFullMetadata( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TSeason> result, |
||||
TSeason incoming, |
||||
bool deepScan); |
||||
|
||||
protected abstract Task<Option<EpisodeMetadata>> GetFullMetadata( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TEpisode> result, |
||||
TEpisode incoming, |
||||
bool deepScan); |
||||
|
||||
protected abstract Task<Either<BaseError, MediaItemScanResult<TShow>>> UpdateMetadata( |
||||
MediaItemScanResult<TShow> result, |
||||
ShowMetadata fullMetadata); |
||||
|
||||
protected abstract Task<Either<BaseError, MediaItemScanResult<TSeason>>> UpdateMetadata( |
||||
MediaItemScanResult<TSeason> result, |
||||
SeasonMetadata fullMetadata); |
||||
|
||||
protected abstract Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateMetadata( |
||||
MediaItemScanResult<TEpisode> result, |
||||
EpisodeMetadata fullMetadata); |
||||
|
||||
private async Task<Either<BaseError, Unit>> ScanSeasons( |
||||
IMediaServerTelevisionRepository<TLibrary, TShow, TSeason, TEpisode, TEtag> televisionRepository, |
||||
TLibrary library, |
||||
Func<TEpisode, string> getLocalPath, |
||||
TShow show, |
||||
TConnectionParameters connectionParameters, |
||||
string ffmpegPath, |
||||
string ffprobePath, |
||||
List<TSeason> seasonEntries, |
||||
bool deepScan, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
List<TEtag> existingSeasons = await televisionRepository.GetExistingSeasons(library, show); |
||||
|
||||
var sortedSeasons = seasonEntries.OrderBy(s => s.SeasonNumber).ToList(); |
||||
foreach (TSeason incoming in sortedSeasons) |
||||
{ |
||||
incoming.ShowId = show.Id; |
||||
|
||||
if (cancellationToken.IsCancellationRequested) |
||||
{ |
||||
return new ScanCanceled(); |
||||
} |
||||
|
||||
Either<BaseError, MediaItemScanResult<TSeason>> maybeSeason = await televisionRepository |
||||
.GetOrAdd(library, incoming) |
||||
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan)); |
||||
|
||||
if (maybeSeason.IsLeft) |
||||
{ |
||||
foreach (BaseError error in maybeSeason.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning( |
||||
"Error processing show {Title} season {SeasonNumber}: {Error}", |
||||
show.ShowMetadata.Head().Title, |
||||
incoming.SeasonNumber, |
||||
error.Value); |
||||
} |
||||
|
||||
continue; |
||||
} |
||||
|
||||
foreach (MediaItemScanResult<TSeason> result in maybeSeason.RightToSeq()) |
||||
{ |
||||
Either<BaseError, List<TEpisode>> entries = await GetEpisodeLibraryItems( |
||||
library, |
||||
connectionParameters, |
||||
result.Item); |
||||
|
||||
foreach (BaseError error in entries.LeftToSeq()) |
||||
{ |
||||
return error; |
||||
} |
||||
|
||||
Either<BaseError, Unit> scanResult = await ScanEpisodes( |
||||
televisionRepository, |
||||
library, |
||||
getLocalPath, |
||||
show, |
||||
result.Item, |
||||
connectionParameters, |
||||
ffmpegPath, |
||||
ffprobePath, |
||||
entries.RightToSeq().Flatten().ToList(), |
||||
deepScan, |
||||
cancellationToken); |
||||
|
||||
foreach (ScanCanceled error in scanResult.LeftToSeq().OfType<ScanCanceled>()) |
||||
{ |
||||
return error; |
||||
} |
||||
|
||||
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming)); |
||||
|
||||
result.Item.Show = show; |
||||
|
||||
if (result.IsAdded || result.IsUpdated) |
||||
{ |
||||
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// trash seasons that are no longer present on the media server
|
||||
var fileNotFoundItemIds = existingSeasons.Map(s => s.MediaServerItemId) |
||||
.Except(seasonEntries.Map(MediaServerItemId)).ToList(); |
||||
List<int> ids = await televisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundItemIds); |
||||
await _searchIndex.RebuildItems(_searchRepository, ids); |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, Unit>> ScanEpisodes( |
||||
IMediaServerTelevisionRepository<TLibrary, TShow, TSeason, TEpisode, TEtag> televisionRepository, |
||||
TLibrary library, |
||||
Func<TEpisode, string> getLocalPath, |
||||
TShow show, |
||||
TSeason season, |
||||
TConnectionParameters connectionParameters, |
||||
string ffmpegPath, |
||||
string ffprobePath, |
||||
List<TEpisode> episodeEntries, |
||||
bool deepScan, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
List<TEtag> existingEpisodes = await televisionRepository.GetExistingEpisodes(library, season); |
||||
|
||||
var sortedEpisodes = episodeEntries.OrderBy(s => s.EpisodeMetadata.Head().EpisodeNumber).ToList(); |
||||
foreach (TEpisode incoming in sortedEpisodes) |
||||
{ |
||||
if (cancellationToken.IsCancellationRequested) |
||||
{ |
||||
return new ScanCanceled(); |
||||
} |
||||
|
||||
string localPath = getLocalPath(incoming); |
||||
if (await ShouldScanItem( |
||||
televisionRepository, |
||||
library, |
||||
show, |
||||
season, |
||||
existingEpisodes, |
||||
incoming, |
||||
localPath, |
||||
deepScan) == false) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
incoming.SeasonId = season.Id; |
||||
|
||||
Either<BaseError, MediaItemScanResult<TEpisode>> maybeEpisode = await televisionRepository |
||||
.GetOrAdd(library, incoming) |
||||
.MapT( |
||||
result => |
||||
{ |
||||
result.LocalPath = localPath; |
||||
return result; |
||||
}) |
||||
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan)) |
||||
.BindT(existing => UpdateStatistics(existing, incoming, ffmpegPath, ffprobePath)) |
||||
.BindT(UpdateSubtitles); |
||||
|
||||
if (maybeEpisode.IsLeft) |
||||
{ |
||||
foreach (BaseError error in maybeEpisode.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning( |
||||
"Error processing episode {Title} s{SeasonNumber:00}e{EpisodeNumber:00}: {Error}", |
||||
show.ShowMetadata.Head().Title, |
||||
season.SeasonNumber, |
||||
incoming.EpisodeMetadata.Head().EpisodeNumber, |
||||
error.Value); |
||||
} |
||||
|
||||
continue; |
||||
} |
||||
|
||||
foreach (MediaItemScanResult<TEpisode> result in maybeEpisode.RightToSeq()) |
||||
{ |
||||
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming)); |
||||
|
||||
if (_localFileSystem.FileExists(result.LocalPath)) |
||||
{ |
||||
if (await televisionRepository.FlagNormal(library, result.Item)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
Option<int> flagResult = await televisionRepository.FlagUnavailable(library, result.Item); |
||||
if (flagResult.IsSome) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
if (result.IsAdded || result.IsUpdated) |
||||
{ |
||||
await _searchIndex.RebuildItems(_searchRepository, new List<int> { result.Item.Id }); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// trash episodes that are no longer present on the media server
|
||||
var fileNotFoundItemIds = existingEpisodes.Map(m => m.MediaServerItemId) |
||||
.Except(episodeEntries.Map(MediaServerItemId)).ToList(); |
||||
List<int> ids = await televisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundItemIds); |
||||
await _searchIndex.RebuildItems(_searchRepository, ids); |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task<bool> ShouldScanItem( |
||||
IMediaServerTelevisionRepository<TLibrary, TShow, TSeason, TEpisode, TEtag> televisionRepository, |
||||
TLibrary library, |
||||
Show show, |
||||
Season season, |
||||
List<TEtag> existingEpisodes, |
||||
TEpisode incoming, |
||||
string localPath, |
||||
bool deepScan) |
||||
{ |
||||
// deep scan will always pull every episode
|
||||
if (deepScan) |
||||
{ |
||||
return true; |
||||
} |
||||
|
||||
Option<TEtag> maybeExisting = existingEpisodes.Find(m => m.MediaServerItemId == MediaServerItemId(incoming)); |
||||
string existingItemId = await maybeExisting.Map(e => e.MediaServerItemId).IfNoneAsync(string.Empty); |
||||
MediaItemState existingState = await maybeExisting.Map(e => e.State).IfNoneAsync(MediaItemState.Normal); |
||||
|
||||
if (existingState == MediaItemState.Unavailable) |
||||
{ |
||||
// skip scanning unavailable items that still don't exist locally
|
||||
if (!_localFileSystem.FileExists(localPath)) |
||||
{ |
||||
return false; |
||||
} |
||||
} |
||||
else if (existingItemId == MediaServerItemId(incoming)) |
||||
{ |
||||
// item is unchanged, but file does not exist
|
||||
// don't scan, but mark as unavailable
|
||||
if (!_localFileSystem.FileExists(localPath)) |
||||
{ |
||||
foreach (int id in await televisionRepository.FlagUnavailable(library, incoming)) |
||||
{ |
||||
await _searchIndex.RebuildItems(_searchRepository, new List<int> { id }); |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
if (maybeExisting.IsNone) |
||||
{ |
||||
_logger.LogDebug( |
||||
"INSERT: new episode {Show} s{SeasonNumber:00}e{EpisodeNumber:00}", |
||||
show.ShowMetadata.Head().Title, |
||||
season.SeasonNumber, |
||||
incoming.EpisodeMetadata.Head().EpisodeNumber); |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogDebug( |
||||
"UPDATE: Etag has changed for episode {Show} s{SeasonNumber:00}e{EpisodeNumber:00}", |
||||
show.ShowMetadata.Head().Title, |
||||
season.SeasonNumber, |
||||
incoming.EpisodeMetadata.Head().EpisodeNumber); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TShow>>> UpdateMetadata( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TShow> result, |
||||
TShow incoming, |
||||
bool deepScan) |
||||
{ |
||||
foreach (ShowMetadata fullMetadata in await GetFullMetadata( |
||||
connectionParameters, |
||||
library, |
||||
result, |
||||
incoming, |
||||
deepScan)) |
||||
{ |
||||
// TODO: move some of this code into this scanner
|
||||
// will have to merge JF, Emby, Plex logic
|
||||
return await UpdateMetadata(result, fullMetadata); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TSeason>>> UpdateMetadata( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TSeason> result, |
||||
TSeason incoming, |
||||
bool deepScan) |
||||
{ |
||||
foreach (SeasonMetadata fullMetadata in await GetFullMetadata( |
||||
connectionParameters, |
||||
library, |
||||
result, |
||||
incoming, |
||||
deepScan)) |
||||
{ |
||||
// TODO: move some of this code into this scanner
|
||||
// will have to merge JF, Emby, Plex logic
|
||||
return await UpdateMetadata(result, fullMetadata); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateMetadata( |
||||
TConnectionParameters connectionParameters, |
||||
TLibrary library, |
||||
MediaItemScanResult<TEpisode> result, |
||||
TEpisode incoming, |
||||
bool deepScan) |
||||
{ |
||||
foreach (EpisodeMetadata fullMetadata in await GetFullMetadata( |
||||
connectionParameters, |
||||
library, |
||||
result, |
||||
incoming, |
||||
deepScan)) |
||||
{ |
||||
// TODO: move some of this code into this scanner
|
||||
// will have to merge JF, Emby, Plex logic
|
||||
return await UpdateMetadata(result, fullMetadata); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateStatistics( |
||||
MediaItemScanResult<TEpisode> result, |
||||
TEpisode incoming, |
||||
string ffmpegPath, |
||||
string ffprobePath) |
||||
{ |
||||
TEpisode existing = result.Item; |
||||
|
||||
if (result.IsAdded || MediaServerItemId(existing) != MediaServerItemId(incoming) || |
||||
existing.MediaVersions.Head().Streams.Count == 0) |
||||
{ |
||||
if (_localFileSystem.FileExists(result.LocalPath)) |
||||
{ |
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", result.LocalPath); |
||||
Either<BaseError, bool> refreshResult = |
||||
await _localStatisticsProvider.RefreshStatistics( |
||||
ffmpegPath, |
||||
ffprobePath, |
||||
existing, |
||||
result.LocalPath); |
||||
|
||||
foreach (BaseError error in refreshResult.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning( |
||||
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}", |
||||
"Statistics", |
||||
result.LocalPath, |
||||
error.Value); |
||||
} |
||||
|
||||
foreach (bool _ in refreshResult.RightToSeq()) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateSubtitles( |
||||
MediaItemScanResult<TEpisode> existing) |
||||
{ |
||||
try |
||||
{ |
||||
// skip checking subtitles for files that don't exist locally
|
||||
if (!_localFileSystem.FileExists(existing.LocalPath)) |
||||
{ |
||||
return existing; |
||||
} |
||||
|
||||
if (await _localSubtitlesProvider.UpdateSubtitles(existing.Item, existing.LocalPath, false)) |
||||
{ |
||||
return existing; |
||||
} |
||||
|
||||
return BaseError.New("Failed to update local subtitles"); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
return BaseError.New(ex.ToString()); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations |
||||
{ |
||||
public partial class Remove_InvalidPlexSeasons : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.Sql(@"DELETE FROM PlexSeason WHERE Key LIKE '%allLeaves%'"); |
||||
} |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue