|
|
|
@ -6,6 +6,7 @@ using ErsatzTV.Core.Errors; |
|
|
|
using ErsatzTV.Core.Extensions; |
|
|
|
using ErsatzTV.Core.Extensions; |
|
|
|
using ErsatzTV.Core.Interfaces.Repositories; |
|
|
|
using ErsatzTV.Core.Interfaces.Repositories; |
|
|
|
using ErsatzTV.Core.Metadata; |
|
|
|
using ErsatzTV.Core.Metadata; |
|
|
|
|
|
|
|
using ErsatzTV.Infrastructure; |
|
|
|
using ErsatzTV.Scanner.Core.Interfaces; |
|
|
|
using ErsatzTV.Scanner.Core.Interfaces; |
|
|
|
using ErsatzTV.Scanner.Core.Interfaces.Metadata; |
|
|
|
using ErsatzTV.Scanner.Core.Interfaces.Metadata; |
|
|
|
using Microsoft.Extensions.Logging; |
|
|
|
using Microsoft.Extensions.Logging; |
|
|
|
@ -95,70 +96,78 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
|
|
|
|
|
|
|
|
await foreach ((TShow incoming, int totalShowCount) in showEntries.WithCancellation(cancellationToken)) |
|
|
|
await foreach ((TShow incoming, int totalShowCount) in showEntries.WithCancellation(cancellationToken)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (cancellationToken.IsCancellationRequested) |
|
|
|
using (ScanProfiler.Measure("Scan Show")) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return new ScanCanceled(); |
|
|
|
if (cancellationToken.IsCancellationRequested) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
incomingItemIds.Add(MediaServerItemId(incoming)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
decimal percentCompletion = Math.Clamp((decimal)incomingItemIds.Count / totalShowCount, 0, 1); |
|
|
|
|
|
|
|
if (!await _scannerProxy.UpdateProgress(percentCompletion, cancellationToken)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return new ScanCanceled(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Either<BaseError, MediaItemScanResult<TShow>> maybeShow = await televisionRepository |
|
|
|
|
|
|
|
.GetOrAdd(library, incoming, cancellationToken) |
|
|
|
|
|
|
|
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (maybeShow.IsLeft) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
foreach (BaseError error in maybeShow.LeftToSeq()) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
_logger.LogWarning( |
|
|
|
return new ScanCanceled(); |
|
|
|
"Error processing show {Title}: {Error}", |
|
|
|
|
|
|
|
incoming.ShowMetadata.Head().Title, |
|
|
|
|
|
|
|
error.Value); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
continue; |
|
|
|
incomingItemIds.Add(MediaServerItemId(incoming)); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (MediaItemScanResult<TShow> result in maybeShow.RightToSeq()) |
|
|
|
decimal percentCompletion = Math.Clamp((decimal)incomingItemIds.Count / totalShowCount, 0, 1); |
|
|
|
{ |
|
|
|
if (!await _scannerProxy.UpdateProgress(percentCompletion, cancellationToken)) |
|
|
|
Either<BaseError, Unit> scanResult = await ScanSeasons( |
|
|
|
|
|
|
|
televisionRepository, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
getLocalPath, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
result.IsUpdated, |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
GetSeasonLibraryItems(library, connectionParameters, result.Item), |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (ScanCanceled error in scanResult.LeftToSeq().OfType<ScanCanceled>()) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
return error; |
|
|
|
return new ScanCanceled(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken); |
|
|
|
Either<BaseError, MediaItemScanResult<TShow>> maybeShow = await televisionRepository |
|
|
|
|
|
|
|
.GetOrAdd(library, incoming, cancellationToken) |
|
|
|
|
|
|
|
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan)); |
|
|
|
|
|
|
|
|
|
|
|
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken); |
|
|
|
if (maybeShow.IsLeft) |
|
|
|
if (flagResult.IsSome) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
result.IsUpdated = true; |
|
|
|
foreach (BaseError error in maybeShow.LeftToSeq()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
_logger.LogWarning( |
|
|
|
|
|
|
|
"Error processing show {Title}: {Error}", |
|
|
|
|
|
|
|
incoming.ShowMetadata.Head().Title, |
|
|
|
|
|
|
|
error.Value); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (result.IsAdded || result.IsUpdated) |
|
|
|
foreach (MediaItemScanResult<TShow> result in maybeShow.RightToSeq()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) |
|
|
|
Either<BaseError, Unit> scanResult = await ScanSeasons( |
|
|
|
|
|
|
|
televisionRepository, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
getLocalPath, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
result.IsUpdated, |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
GetSeasonLibraryItems(library, connectionParameters, result.Item), |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (ScanCanceled error in scanResult.LeftToSeq().OfType<ScanCanceled>()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
_logger.LogWarning("Failed to reindex media items from scanner process"); |
|
|
|
return error; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Option<int> flagResult = await televisionRepository.FlagNormal( |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
if (flagResult.IsSome) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.IsUpdated = true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (result.IsAdded || result.IsUpdated) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
_logger.LogWarning("Failed to reindex media items from scanner process"); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ScanProfiler.LogStatistics(s => _logger.LogInformation("{Profile}", s)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (cleanupFileNotFoundItems) |
|
|
|
if (cleanupFileNotFoundItems) |
|
|
|
@ -285,67 +294,73 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
|
|
|
|
|
|
|
|
await foreach ((TSeason incoming, int _) in seasonEntries.WithCancellation(cancellationToken)) |
|
|
|
await foreach ((TSeason incoming, int _) in seasonEntries.WithCancellation(cancellationToken)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
incoming.ShowId = show.Id; |
|
|
|
using (ScanProfiler.Measure("Scan Season")) |
|
|
|
|
|
|
|
|
|
|
|
if (cancellationToken.IsCancellationRequested) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
return new ScanCanceled(); |
|
|
|
incoming.ShowId = show.Id; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
incomingItemIds.Add(MediaServerItemId(incoming)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Either<BaseError, MediaItemScanResult<TSeason>> maybeSeason = await televisionRepository |
|
|
|
|
|
|
|
.GetOrAdd(library, incoming, cancellationToken) |
|
|
|
|
|
|
|
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (maybeSeason.IsLeft) |
|
|
|
if (cancellationToken.IsCancellationRequested) |
|
|
|
{ |
|
|
|
|
|
|
|
foreach (BaseError error in maybeSeason.LeftToSeq()) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
_logger.LogWarning( |
|
|
|
return new ScanCanceled(); |
|
|
|
"Error processing show {Title} season {SeasonNumber}: {Error}", |
|
|
|
|
|
|
|
show.ShowMetadata.Head().Title, |
|
|
|
|
|
|
|
incoming.SeasonNumber, |
|
|
|
|
|
|
|
error.Value); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
continue; |
|
|
|
incomingItemIds.Add(MediaServerItemId(incoming)); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (MediaItemScanResult<TSeason> result in maybeSeason.RightToSeq()) |
|
|
|
Either<BaseError, MediaItemScanResult<TSeason>> maybeSeason = await televisionRepository |
|
|
|
{ |
|
|
|
.GetOrAdd(library, incoming, cancellationToken) |
|
|
|
Either<BaseError, Unit> scanResult = await ScanEpisodes( |
|
|
|
.BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan)); |
|
|
|
televisionRepository, |
|
|
|
|
|
|
|
library, |
|
|
|
if (maybeSeason.IsLeft) |
|
|
|
getLocalPath, |
|
|
|
|
|
|
|
show, |
|
|
|
|
|
|
|
showIsUpdated, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
GetEpisodeLibraryItems(library, connectionParameters, show, result.Item, result.IsAdded), |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (ScanCanceled error in scanResult.LeftToSeq().OfType<ScanCanceled>()) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
return error; |
|
|
|
foreach (BaseError error in maybeSeason.LeftToSeq()) |
|
|
|
} |
|
|
|
{ |
|
|
|
|
|
|
|
_logger.LogWarning( |
|
|
|
|
|
|
|
"Error processing show {Title} season {SeasonNumber}: {Error}", |
|
|
|
|
|
|
|
show.ShowMetadata.Head().Title, |
|
|
|
|
|
|
|
incoming.SeasonNumber, |
|
|
|
|
|
|
|
error.Value); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken); |
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken); |
|
|
|
foreach (MediaItemScanResult<TSeason> result in maybeSeason.RightToSeq()) |
|
|
|
if (flagResult.IsSome) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
result.IsUpdated = true; |
|
|
|
Either<BaseError, Unit> scanResult = await ScanEpisodes( |
|
|
|
} |
|
|
|
televisionRepository, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
getLocalPath, |
|
|
|
|
|
|
|
show, |
|
|
|
|
|
|
|
showIsUpdated, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
GetEpisodeLibraryItems(library, connectionParameters, show, result.Item, result.IsAdded), |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
|
|
|
|
result.Item.Show = show; |
|
|
|
foreach (ScanCanceled error in scanResult.LeftToSeq().OfType<ScanCanceled>()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return error; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (result.IsAdded || result.IsUpdated || showIsUpdated) |
|
|
|
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken); |
|
|
|
{ |
|
|
|
|
|
|
|
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) |
|
|
|
Option<int> flagResult = await televisionRepository.FlagNormal( |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
if (flagResult.IsSome) |
|
|
|
{ |
|
|
|
{ |
|
|
|
_logger.LogWarning("Failed to reindex media items from scanner process"); |
|
|
|
result.IsUpdated = true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result.Item.Show = show; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (result.IsAdded || result.IsUpdated || showIsUpdated) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
_logger.LogWarning("Failed to reindex media items from scanner process"); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -379,129 +394,141 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
|
|
|
|
|
|
|
|
await foreach ((TEpisode incoming, int _) in episodeEntries.WithCancellation(cancellationToken)) |
|
|
|
await foreach ((TEpisode incoming, int _) in episodeEntries.WithCancellation(cancellationToken)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (cancellationToken.IsCancellationRequested) |
|
|
|
using (ScanProfiler.Measure("Scan Episode")) |
|
|
|
{ |
|
|
|
|
|
|
|
return new ScanCanceled(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
incomingItemIds.Add(MediaServerItemId(incoming)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string localPath = getLocalPath(incoming); |
|
|
|
|
|
|
|
if (!await ShouldScanItem( |
|
|
|
|
|
|
|
televisionRepository, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
show, |
|
|
|
|
|
|
|
season, |
|
|
|
|
|
|
|
existingEpisodes, |
|
|
|
|
|
|
|
incoming, |
|
|
|
|
|
|
|
localPath, |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
cancellationToken)) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
continue; |
|
|
|
if (cancellationToken.IsCancellationRequested) |
|
|
|
} |
|
|
|
{ |
|
|
|
|
|
|
|
return new ScanCanceled(); |
|
|
|
incoming.SeasonId = season.Id; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Either<BaseError, MediaItemScanResult<TEpisode>> maybeEpisode; |
|
|
|
incomingItemIds.Add(MediaServerItemId(incoming)); |
|
|
|
|
|
|
|
|
|
|
|
if (ServerReturnsStatisticsWithMetadata) |
|
|
|
string localPath = getLocalPath(incoming); |
|
|
|
{ |
|
|
|
if (!await ShouldScanItem( |
|
|
|
maybeEpisode = await televisionRepository |
|
|
|
televisionRepository, |
|
|
|
.GetOrAdd(library, incoming, deepScan, cancellationToken) |
|
|
|
|
|
|
|
.MapT(result => |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.LocalPath = localPath; |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.BindT(existing => UpdateMetadataAndStatistics( |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
existing, |
|
|
|
|
|
|
|
incoming, |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
cancellationToken)) |
|
|
|
|
|
|
|
.BindT(existing => UpdateChapters(existing, cancellationToken)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
maybeEpisode = await televisionRepository |
|
|
|
|
|
|
|
.GetOrAdd(library, incoming, deepScan, cancellationToken) |
|
|
|
|
|
|
|
.MapT(result => |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.LocalPath = localPath; |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.BindT(existing => UpdateMetadata( |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
library, |
|
|
|
existing, |
|
|
|
show, |
|
|
|
|
|
|
|
season, |
|
|
|
|
|
|
|
existingEpisodes, |
|
|
|
incoming, |
|
|
|
incoming, |
|
|
|
|
|
|
|
localPath, |
|
|
|
deepScan, |
|
|
|
deepScan, |
|
|
|
None, |
|
|
|
|
|
|
|
cancellationToken)) |
|
|
|
cancellationToken)) |
|
|
|
.BindT(existing => UpdateStatistics( |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
existing, |
|
|
|
|
|
|
|
incoming, |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
None)) |
|
|
|
|
|
|
|
.BindT(existing => UpdateSubtitles(existing, cancellationToken)) |
|
|
|
|
|
|
|
.BindT(existing => UpdateChapters(existing, cancellationToken)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (maybeEpisode.IsLeft) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
foreach (BaseError error in maybeEpisode.LeftToSeq()) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
_logger.LogWarning( |
|
|
|
continue; |
|
|
|
"Error processing episode {Title} s{SeasonNumber:00}e{EpisodeNumber:00}: {Error}", |
|
|
|
|
|
|
|
show.ShowMetadata.Head().Title, |
|
|
|
|
|
|
|
season.SeasonNumber, |
|
|
|
|
|
|
|
incoming.EpisodeMetadata.Head().EpisodeNumber, |
|
|
|
|
|
|
|
error.Value); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
continue; |
|
|
|
incoming.SeasonId = season.Id; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (MediaItemScanResult<TEpisode> result in maybeEpisode.RightToSeq()) |
|
|
|
Either<BaseError, MediaItemScanResult<TEpisode>> maybeEpisode; |
|
|
|
{ |
|
|
|
|
|
|
|
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_fileSystem.File.Exists(result.LocalPath)) |
|
|
|
if (ServerReturnsStatisticsWithMetadata) |
|
|
|
{ |
|
|
|
{ |
|
|
|
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken); |
|
|
|
maybeEpisode = await televisionRepository |
|
|
|
if (flagResult.IsSome) |
|
|
|
.GetOrAdd(library, incoming, deepScan, cancellationToken) |
|
|
|
{ |
|
|
|
.MapT(result => |
|
|
|
result.IsUpdated = true; |
|
|
|
{ |
|
|
|
} |
|
|
|
result.LocalPath = localPath; |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.BindT(existing => UpdateMetadataAndStatistics( |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
existing, |
|
|
|
|
|
|
|
incoming, |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
cancellationToken)) |
|
|
|
|
|
|
|
.BindT(existing => UpdateChapters(existing, cancellationToken)); |
|
|
|
} |
|
|
|
} |
|
|
|
else if (ServerSupportsRemoteStreaming) |
|
|
|
else |
|
|
|
{ |
|
|
|
{ |
|
|
|
Option<int> flagResult = await televisionRepository.FlagRemoteOnly(library, result.Item, cancellationToken); |
|
|
|
maybeEpisode = await televisionRepository |
|
|
|
if (flagResult.IsSome) |
|
|
|
.GetOrAdd(library, incoming, deepScan, cancellationToken) |
|
|
|
{ |
|
|
|
.MapT(result => |
|
|
|
result.IsUpdated = true; |
|
|
|
{ |
|
|
|
} |
|
|
|
result.LocalPath = localPath; |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.BindT(existing => UpdateMetadata( |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
existing, |
|
|
|
|
|
|
|
incoming, |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
None, |
|
|
|
|
|
|
|
cancellationToken)) |
|
|
|
|
|
|
|
.BindT(existing => UpdateStatistics( |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
existing, |
|
|
|
|
|
|
|
incoming, |
|
|
|
|
|
|
|
deepScan, |
|
|
|
|
|
|
|
None)) |
|
|
|
|
|
|
|
.BindT(existing => UpdateSubtitles(existing, cancellationToken)) |
|
|
|
|
|
|
|
.BindT(existing => UpdateChapters(existing, cancellationToken)); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
|
|
|
|
|
|
|
|
if (maybeEpisode.IsLeft) |
|
|
|
{ |
|
|
|
{ |
|
|
|
Option<int> flagResult = await televisionRepository.FlagUnavailable(library, result.Item, cancellationToken); |
|
|
|
foreach (BaseError error in maybeEpisode.LeftToSeq()) |
|
|
|
if (flagResult.IsSome) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
result.IsUpdated = true; |
|
|
|
_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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (result.IsAdded || result.IsUpdated || showIsUpdated) |
|
|
|
foreach (MediaItemScanResult<TEpisode> result in maybeEpisode.RightToSeq()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) |
|
|
|
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_fileSystem.File.Exists(result.LocalPath)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
_logger.LogWarning("Failed to reindex media items from scanner process"); |
|
|
|
Option<int> flagResult = await televisionRepository.FlagNormal( |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
if (flagResult.IsSome) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.IsUpdated = true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else if (ServerSupportsRemoteStreaming) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
Option<int> flagResult = await televisionRepository.FlagRemoteOnly( |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
if (flagResult.IsSome) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.IsUpdated = true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
Option<int> flagResult = await televisionRepository.FlagUnavailable( |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result.Item, |
|
|
|
|
|
|
|
cancellationToken); |
|
|
|
|
|
|
|
if (flagResult.IsSome) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.IsUpdated = true; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (result.IsAdded || result.IsUpdated || showIsUpdated) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
_logger.LogWarning("Failed to reindex media items from scanner process"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -611,16 +638,19 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
TShow incoming, |
|
|
|
TShow incoming, |
|
|
|
bool deepScan) |
|
|
|
bool deepScan) |
|
|
|
{ |
|
|
|
{ |
|
|
|
foreach (ShowMetadata fullMetadata in await GetFullMetadata( |
|
|
|
using (ScanProfiler.Measure("Update Show Metadata")) |
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result, |
|
|
|
|
|
|
|
incoming, |
|
|
|
|
|
|
|
deepScan)) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
// TODO: move some of this code into this scanner
|
|
|
|
foreach (ShowMetadata fullMetadata in await GetFullMetadata( |
|
|
|
// will have to merge JF, Emby, Plex logic
|
|
|
|
connectionParameters, |
|
|
|
return await UpdateMetadata(result, fullMetadata); |
|
|
|
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; |
|
|
|
return result; |
|
|
|
@ -633,16 +663,19 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
TSeason incoming, |
|
|
|
TSeason incoming, |
|
|
|
bool deepScan) |
|
|
|
bool deepScan) |
|
|
|
{ |
|
|
|
{ |
|
|
|
foreach (SeasonMetadata fullMetadata in await GetFullMetadata( |
|
|
|
using (ScanProfiler.Measure("Update Season Metadata")) |
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result, |
|
|
|
|
|
|
|
incoming, |
|
|
|
|
|
|
|
deepScan)) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
// TODO: move some of this code into this scanner
|
|
|
|
foreach (SeasonMetadata fullMetadata in await GetFullMetadata( |
|
|
|
// will have to merge JF, Emby, Plex logic
|
|
|
|
connectionParameters, |
|
|
|
return await UpdateMetadata(result, fullMetadata); |
|
|
|
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; |
|
|
|
return result; |
|
|
|
@ -714,19 +747,22 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
Option<EpisodeMetadata> maybeFullMetadata, |
|
|
|
Option<EpisodeMetadata> maybeFullMetadata, |
|
|
|
CancellationToken cancellationToken) |
|
|
|
CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (maybeFullMetadata.IsNone) |
|
|
|
using (ScanProfiler.Measure("Update Episode Metadata")) |
|
|
|
{ |
|
|
|
{ |
|
|
|
maybeFullMetadata = await GetFullMetadata(connectionParameters, library, result, incoming, deepScan); |
|
|
|
if (maybeFullMetadata.IsNone) |
|
|
|
} |
|
|
|
{ |
|
|
|
|
|
|
|
maybeFullMetadata = await GetFullMetadata(connectionParameters, library, result, incoming, deepScan); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
foreach (EpisodeMetadata fullMetadata in maybeFullMetadata) |
|
|
|
foreach (EpisodeMetadata fullMetadata in maybeFullMetadata) |
|
|
|
{ |
|
|
|
{ |
|
|
|
// TODO: move some of this code into this scanner
|
|
|
|
// TODO: move some of this code into this scanner
|
|
|
|
// will have to merge JF, Emby, Plex logic
|
|
|
|
// will have to merge JF, Emby, Plex logic
|
|
|
|
return await UpdateMetadata(result, fullMetadata, cancellationToken); |
|
|
|
return await UpdateMetadata(result, fullMetadata, cancellationToken); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateStatistics( |
|
|
|
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateStatistics( |
|
|
|
@ -737,57 +773,60 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
bool deepScan, |
|
|
|
bool deepScan, |
|
|
|
Option<MediaVersion> maybeMediaVersion) |
|
|
|
Option<MediaVersion> maybeMediaVersion) |
|
|
|
{ |
|
|
|
{ |
|
|
|
TEpisode existing = result.Item; |
|
|
|
using (ScanProfiler.Measure("Update Episode Stats")) |
|
|
|
|
|
|
|
|
|
|
|
if (deepScan || result.IsAdded || MediaServerEtag(existing) != MediaServerEtag(incoming) || |
|
|
|
|
|
|
|
existing.MediaVersions.Head().Streams.Count == 0) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
// if (maybeMediaVersion.IsNone && _fileSystem.File.Exists(result.LocalPath))
|
|
|
|
TEpisode existing = result.Item; |
|
|
|
// {
|
|
|
|
|
|
|
|
// _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;
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// else
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
if (maybeMediaVersion.IsNone) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
maybeMediaVersion = await GetMediaServerStatistics( |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result, |
|
|
|
|
|
|
|
incoming); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (MediaVersion mediaVersion in maybeMediaVersion) |
|
|
|
if (deepScan || result.IsAdded || MediaServerEtag(existing) != MediaServerEtag(incoming) || |
|
|
|
|
|
|
|
existing.MediaVersions.Head().Streams.Count == 0) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (await _metadataRepository.UpdateStatistics(result.Item, mediaVersion)) |
|
|
|
// if (maybeMediaVersion.IsNone && _fileSystem.File.Exists(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;
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// else
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
if (maybeMediaVersion.IsNone) |
|
|
|
{ |
|
|
|
{ |
|
|
|
result.IsUpdated = true; |
|
|
|
maybeMediaVersion = await GetMediaServerStatistics( |
|
|
|
|
|
|
|
connectionParameters, |
|
|
|
|
|
|
|
library, |
|
|
|
|
|
|
|
result, |
|
|
|
|
|
|
|
incoming); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (MediaVersion mediaVersion in maybeMediaVersion) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (await _metadataRepository.UpdateStatistics(result.Item, mediaVersion)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.IsUpdated = true; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// }
|
|
|
|
} |
|
|
|
} |
|
|
|
// }
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateSubtitles( |
|
|
|
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateSubtitles( |
|
|
|
@ -796,22 +835,25 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
{ |
|
|
|
{ |
|
|
|
try |
|
|
|
try |
|
|
|
{ |
|
|
|
{ |
|
|
|
MediaVersion version = existing.Item.GetHeadVersion(); |
|
|
|
using (ScanProfiler.Measure("Update Episode Subtitles")) |
|
|
|
Option<EpisodeMetadata> maybeMetadata = existing.Item.EpisodeMetadata.HeadOrNone(); |
|
|
|
|
|
|
|
foreach (EpisodeMetadata metadata in maybeMetadata) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
List<Subtitle> subtitles = version.Streams |
|
|
|
MediaVersion version = existing.Item.GetHeadVersion(); |
|
|
|
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) |
|
|
|
Option<EpisodeMetadata> maybeMetadata = existing.Item.EpisodeMetadata.HeadOrNone(); |
|
|
|
.Map(Subtitle.FromMediaStream) |
|
|
|
foreach (EpisodeMetadata metadata in maybeMetadata) |
|
|
|
.ToList(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (await _metadataRepository.UpdateSubtitles(metadata, subtitles, cancellationToken)) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
return existing; |
|
|
|
List<Subtitle> subtitles = version.Streams |
|
|
|
|
|
|
|
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) |
|
|
|
|
|
|
|
.Map(Subtitle.FromMediaStream) |
|
|
|
|
|
|
|
.ToList(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (await _metadataRepository.UpdateSubtitles(metadata, subtitles, cancellationToken)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return existing; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return BaseError.New("Failed to update media server subtitles"); |
|
|
|
return BaseError.New("Failed to update media server subtitles"); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
catch (Exception ex) |
|
|
|
catch (Exception ex) |
|
|
|
{ |
|
|
|
{ |
|
|
|
@ -825,18 +867,24 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, |
|
|
|
{ |
|
|
|
{ |
|
|
|
try |
|
|
|
try |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (string.IsNullOrEmpty(existing.LocalPath)) |
|
|
|
using (ScanProfiler.Measure("Update Episode Chapters")) |
|
|
|
{ |
|
|
|
{ |
|
|
|
// No local path available for external chapter file lookup
|
|
|
|
if (string.IsNullOrEmpty(existing.LocalPath)) |
|
|
|
return existing; |
|
|
|
{ |
|
|
|
} |
|
|
|
// No local path available for external chapter file lookup
|
|
|
|
|
|
|
|
return existing; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (await _localChaptersProvider.UpdateChapters(existing.Item, Some(existing.LocalPath), cancellationToken)) |
|
|
|
if (await _localChaptersProvider.UpdateChapters( |
|
|
|
{ |
|
|
|
existing.Item, |
|
|
|
existing.IsUpdated = true; |
|
|
|
Some(existing.LocalPath), |
|
|
|
} |
|
|
|
cancellationToken)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
existing.IsUpdated = true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return existing; |
|
|
|
return existing; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
catch (Exception ex) |
|
|
|
catch (Exception ex) |
|
|
|
{ |
|
|
|
{ |
|
|
|
|