mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
10 KiB
222 lines
10 KiB
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Threading.Tasks; |
|
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Interfaces.Metadata; |
|
using ErsatzTV.Core.Interfaces.Repositories; |
|
using ErsatzTV.Core.Interfaces.Scheduling; |
|
using LanguageExt; |
|
using Microsoft.Extensions.Logging; |
|
using static LanguageExt.Prelude; |
|
|
|
namespace ErsatzTV.Core.Metadata |
|
{ |
|
public class LocalMediaScanner : ILocalMediaScanner |
|
{ |
|
private readonly ILocalFileSystem _localFileSystem; |
|
private readonly ILocalMetadataProvider _localMetadataProvider; |
|
private readonly ILocalPosterProvider _localPosterProvider; |
|
private readonly ILocalStatisticsProvider _localStatisticsProvider; |
|
private readonly ILogger<LocalMediaScanner> _logger; |
|
private readonly IMediaItemRepository _mediaItemRepository; |
|
private readonly IPlayoutBuilder _playoutBuilder; |
|
private readonly IPlayoutRepository _playoutRepository; |
|
private readonly ISmartCollectionBuilder _smartCollectionBuilder; |
|
|
|
public LocalMediaScanner( |
|
IMediaItemRepository mediaItemRepository, |
|
IPlayoutRepository playoutRepository, |
|
ILocalStatisticsProvider localStatisticsProvider, |
|
ILocalMetadataProvider localMetadataProvider, |
|
ILocalPosterProvider localPosterProvider, |
|
ISmartCollectionBuilder smartCollectionBuilder, |
|
IPlayoutBuilder playoutBuilder, |
|
ILogger<LocalMediaScanner> logger, |
|
ILocalFileSystem localFileSystem) |
|
{ |
|
_mediaItemRepository = mediaItemRepository; |
|
_playoutRepository = playoutRepository; |
|
_localStatisticsProvider = localStatisticsProvider; |
|
_localMetadataProvider = localMetadataProvider; |
|
_localPosterProvider = localPosterProvider; |
|
_smartCollectionBuilder = smartCollectionBuilder; |
|
_playoutBuilder = playoutBuilder; |
|
_logger = logger; |
|
_localFileSystem = localFileSystem; |
|
} |
|
|
|
public async Task<Unit> ScanLocalMediaSource( |
|
LocalMediaSource localMediaSource, |
|
string ffprobePath, |
|
ScanningMode scanningMode) |
|
{ |
|
if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource)) |
|
{ |
|
_logger.LogWarning( |
|
"Media source folder {Folder} does not exist or is inaccessible; skipping scan", |
|
localMediaSource.Folder); |
|
return unit; |
|
} |
|
|
|
List<MediaItem> knownMediaItems = await _mediaItemRepository.GetAllByMediaSourceId(localMediaSource.Id); |
|
var modifiedPlayoutIds = new List<int>(); |
|
|
|
Seq<string> allFiles = _localFileSystem.FindRelevantVideos(localMediaSource); |
|
|
|
var testScanner = new TestMediaScanner(_localFileSystem); |
|
var actions = testScanner.DetermineActions( |
|
localMediaSource.MediaType, |
|
knownMediaItems.ToSeq(), |
|
FindAllFiles(localMediaSource)); |
|
|
|
foreach (var action in actions) |
|
{ |
|
foreach (var plan in action.ActionPlans) |
|
{ |
|
_logger.LogDebug( |
|
"Action Plan: {Source} / {File} => {Action}", |
|
action.Source.Match( |
|
Right: mediaItem => Path.GetFileName(mediaItem.Path), |
|
Left: Path.GetFileName), |
|
plan.TargetPath, |
|
plan.TargetAction); |
|
} |
|
} |
|
|
|
// // check if the media item exists |
|
// (Seq<string> newFiles, Seq<MediaItem> existingMediaItems) = allFiles.Map( |
|
// s => Optional(knownMediaItems.Find(i => i.Path == s)).ToEither(s)) |
|
// .Partition(); |
|
// |
|
// // remove media items that no longer exist |
|
// var missingMediaItems = knownMediaItems.Filter(i => !allFiles.Contains(i.Path)).ToSeq(); |
|
// await RemoveMissingItems(missingMediaItems); |
|
// modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(missingMediaItems)); |
|
// |
|
// Seq<MediaItem> staleMetadataMediaItems = scanningMode == ScanningMode.RescanAll |
|
// ? existingMediaItems |
|
// : existingMediaItems.Filter(i => _localFileSystem.ShouldRefreshMetadata(localMediaSource, i)); |
|
// Seq<MediaItem> modifiedMediaItems = await RefreshMetadataForItems(ffprobePath, staleMetadataMediaItems); |
|
// modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems)); |
|
// |
|
// // if new, add and store mtime, refresh metadata |
|
// var addedMediaItems = new List<MediaItem>(); |
|
// foreach (string path in newFiles) |
|
// { |
|
// _logger.LogDebug("Adding new media item {MediaItem}", path); |
|
// var mediaItem = new MediaItem |
|
// { |
|
// MediaSourceId = localMediaSource.Id, |
|
// Path = path, |
|
// LastWriteTime = File.GetLastWriteTimeUtc(path) |
|
// }; |
|
// |
|
// await _mediaItemRepository.Add(mediaItem); |
|
// await RefreshMetadata(mediaItem, ffprobePath); |
|
// addedMediaItems.Add(mediaItem); |
|
// } |
|
// |
|
// modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(addedMediaItems.ToSeq())); |
|
// |
|
// Seq<MediaItem> stalePosterMediaItems = existingMediaItems |
|
// .Filter(_localFileSystem.ShouldRefreshPoster) |
|
// .Concat(addedMediaItems); |
|
// await RefreshPosterForItems(stalePosterMediaItems); |
|
// |
|
// foreach (int playoutId in modifiedPlayoutIds.Distinct()) |
|
// { |
|
// Option<Playout> maybePlayout = await _playoutRepository.GetFull(playoutId); |
|
// await maybePlayout.Match( |
|
// async playout => |
|
// { |
|
// Playout result = await _playoutBuilder.BuildPlayoutItems(playout, true); |
|
// await _playoutRepository.Update(result); |
|
// }, |
|
// Task.CompletedTask); |
|
// } |
|
|
|
return unit; |
|
} |
|
|
|
private Seq<string> FindAllFiles(LocalMediaSource localMediaSource) |
|
{ |
|
Seq<string> allDirectories = Directory |
|
.GetDirectories(localMediaSource.Folder, "*", SearchOption.AllDirectories) |
|
.ToSeq() |
|
.Add(localMediaSource.Folder); |
|
|
|
// remove any directories with an .etvignore file locally, or in any parent directory |
|
Seq<string> excluded = allDirectories.Filter(path => File.Exists(Path.Combine(path, ".etvignore"))); |
|
Seq<string> relevantDirectories = allDirectories |
|
.Filter(d => !excluded.Any(d.StartsWith)); |
|
// .Filter(d => localMediaSource.MediaType == MediaType.Other || !IsExtrasFolder(d)); |
|
|
|
return relevantDirectories |
|
.Collect(d => Directory.GetFiles(d, "*", SearchOption.TopDirectoryOnly)) |
|
.OrderBy(identity) |
|
.ToSeq(); |
|
} |
|
|
|
private async Task<Seq<MediaItem>> RefreshMetadataForItems( |
|
string ffprobePath, |
|
Seq<MediaItem> staleMetadataMediaItems) |
|
{ |
|
var modifiedMediaItems = new List<MediaItem>(); |
|
foreach (MediaItem mediaItem in staleMetadataMediaItems) |
|
{ |
|
_logger.LogDebug("Refreshing metadata for media item {MediaItem}", mediaItem.Path); |
|
if (await RefreshMetadata(mediaItem, ffprobePath)) |
|
{ |
|
// only queue playout rebuilds for media items |
|
// where the duration or collections have changed |
|
modifiedMediaItems.Add(mediaItem); |
|
} |
|
} |
|
|
|
return modifiedMediaItems.ToSeq(); |
|
} |
|
|
|
private async Task RefreshPosterForItems(Seq<MediaItem> stalePosterMediaItems) |
|
{ |
|
(Seq<MediaItem> movies, Seq<MediaItem> episodes) = stalePosterMediaItems |
|
.Map(i => Optional(i).Filter(i2 => i2.Metadata?.MediaType == MediaType.TvShow).ToEither(i)) |
|
.Partition(); |
|
|
|
// there's a 1:1 movie:poster, so refresh all |
|
foreach (MediaItem movie in movies) |
|
{ |
|
_logger.LogDebug("Refreshing poster for media item {MediaItem}", movie.Path); |
|
await _localPosterProvider.RefreshPoster(movie); |
|
} |
|
|
|
// we currently have 1 poster per series, so pick the first from each group |
|
IEnumerable<MediaItem> episodesToRefresh = episodes.GroupBy(e => e.Metadata.Title) |
|
.SelectMany(g => (Option<MediaItem>) g.FirstOrDefault()); |
|
|
|
foreach (MediaItem episode in episodesToRefresh) |
|
{ |
|
_logger.LogDebug("Refreshing poster for media item {MediaItem}", episode.Path); |
|
await _localPosterProvider.RefreshPoster(episode); |
|
} |
|
} |
|
|
|
private async Task RemoveMissingItems(Seq<MediaItem> removedMediaItems) |
|
{ |
|
// TODO: flag as missing? delete after some period of time? |
|
foreach (MediaItem mediaItem in removedMediaItems) |
|
{ |
|
_logger.LogDebug("Removing missing local media item {MediaItem}", mediaItem.Path); |
|
await _mediaItemRepository.Delete(mediaItem.Id); |
|
} |
|
} |
|
|
|
private async Task<bool> RefreshMetadata(MediaItem mediaItem, string ffprobePath) |
|
{ |
|
bool durationChange = await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem); |
|
await _localMetadataProvider.RefreshMetadata(mediaItem); |
|
bool collectionChange = await _smartCollectionBuilder.RefreshSmartCollections(mediaItem); |
|
return durationChange || collectionChange; |
|
} |
|
} |
|
}
|
|
|