rewrite local media scanner (#25)

* spike new scanner

* add existing items to new scanner

* add collection refresh actions

* add tv show metadata and posters

* update metadata and posters when nfo/poster files are updated

* add "remove" action, test for all supported file extensions

* update statistics when primary video file is updated

* reflect that collections are "sourced" from nfo

* implement most scanning actions

* cleanup

* fix startup

* cross-platform scanner tests
This commit is contained in:
Jason Dove
2021-02-15 23:55:19 +00:00
committed by GitHub
parent 1aac2f13c9
commit 8fb23f2edb
24 changed files with 2422 additions and 105 deletions
+4
View File
@@ -0,0 +1,4 @@
namespace ErsatzTV.Core.Metadata
{
public record ActionPlan(string TargetPath, ScanningAction TargetAction);
}
@@ -10,7 +10,7 @@ namespace ErsatzTV.Core.Metadata
public static MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
{
string fileName = Path.GetFileName(mediaItem.Path);
var metadata = new MediaMetadata { Title = fileName ?? mediaItem.Path };
var metadata = new MediaMetadata { Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path };
if (fileName != null)
{
@@ -10,6 +10,9 @@ namespace ErsatzTV.Core.Metadata
{
public class LocalFileSystem : ILocalFileSystem
{
public DateTime GetLastWriteTime(string path) =>
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => DateTime.MinValue);
public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) =>
Directory.Exists(localMediaSource.Folder);
+193 -87
View File
@@ -1,22 +1,26 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images;
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;
using Seq = LanguageExt.Seq;
namespace ErsatzTV.Core.Metadata
{
public class LocalMediaScanner : ILocalMediaScanner
{
private readonly IImageCache _imageCache;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMediaSourcePlanner _localMediaSourcePlanner;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalPosterProvider _localPosterProvider;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILogger<LocalMediaScanner> _logger;
private readonly IMediaItemRepository _mediaItemRepository;
@@ -29,21 +33,23 @@ namespace ErsatzTV.Core.Metadata
IPlayoutRepository playoutRepository,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
ILocalPosterProvider localPosterProvider,
ISmartCollectionBuilder smartCollectionBuilder,
IPlayoutBuilder playoutBuilder,
ILogger<LocalMediaScanner> logger,
ILocalFileSystem localFileSystem)
ILocalMediaSourcePlanner localMediaSourcePlanner,
ILocalFileSystem localFileSystem,
IImageCache imageCache,
ILogger<LocalMediaScanner> logger)
{
_mediaItemRepository = mediaItemRepository;
_playoutRepository = playoutRepository;
_localStatisticsProvider = localStatisticsProvider;
_localMetadataProvider = localMetadataProvider;
_localPosterProvider = localPosterProvider;
_smartCollectionBuilder = smartCollectionBuilder;
_playoutBuilder = playoutBuilder;
_logger = logger;
_localMediaSourcePlanner = localMediaSourcePlanner;
_localFileSystem = localFileSystem;
_imageCache = imageCache;
_logger = logger;
}
public async Task<Unit> ScanLocalMediaSource(
@@ -62,48 +68,78 @@ namespace ErsatzTV.Core.Metadata
List<MediaItem> knownMediaItems = await _mediaItemRepository.GetAllByMediaSourceId(localMediaSource.Id);
var modifiedPlayoutIds = new List<int>();
Seq<string> allFiles = _localFileSystem.FindRelevantVideos(localMediaSource);
Seq<LocalMediaSourcePlan> actions = _localMediaSourcePlanner.DetermineActions(
localMediaSource.MediaType,
knownMediaItems.ToSeq(),
FindAllFiles(localMediaSource));
// 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)
foreach (LocalMediaSourcePlan action in actions)
{
_logger.LogDebug("Adding new media item {MediaItem}", path);
var mediaItem = new MediaItem
Option<ActionPlan> maybeAddPlan =
action.ActionPlans.SingleOrDefault(plan => plan.TargetAction == ScanningAction.Add);
await maybeAddPlan.IfSomeAsync(
async plan =>
{
Option<MediaItem> maybeMediaItem = await AddMediaItem(localMediaSource, plan.TargetPath);
// any actions other than "add" need to operate on a media item
maybeMediaItem.IfSome(mediaItem => action.Source = mediaItem);
});
foreach (ActionPlan plan in action.ActionPlans.OrderBy(plan => (int) plan.TargetAction))
{
MediaSourceId = localMediaSource.Id,
Path = path,
LastWriteTime = File.GetLastWriteTimeUtc(path)
};
string sourcePath = action.Source.Match(
mediaItem => mediaItem.Path,
path => path);
await _mediaItemRepository.Add(mediaItem);
await RefreshMetadata(mediaItem, ffprobePath);
addedMediaItems.Add(mediaItem);
_logger.LogDebug(
"{Source}: {Action} with {File}",
Path.GetFileName(sourcePath),
plan.TargetAction,
Path.GetRelativePath(Path.GetDirectoryName(sourcePath) ?? string.Empty, plan.TargetPath));
await action.Source.Match(
async mediaItem =>
{
var changed = false;
switch (plan.TargetAction)
{
case ScanningAction.Remove:
await RemoveMissingItem(mediaItem);
break;
case ScanningAction.Poster:
await SavePosterForItem(mediaItem, plan.TargetPath);
break;
case ScanningAction.FallbackMetadata:
await RefreshFallbackMetadataForItem(mediaItem);
break;
case ScanningAction.SidecarMetadata:
await RefreshSidecarMetadataForItem(mediaItem, plan.TargetPath);
break;
case ScanningAction.Statistics:
changed = await RefreshStatisticsForItem(mediaItem, ffprobePath);
break;
case ScanningAction.Collections:
changed = await RefreshCollectionsForItem(mediaItem);
break;
}
if (changed)
{
List<int> ids =
await _playoutRepository.GetPlayoutIdsForMediaItems(Seq.create(mediaItem));
modifiedPlayoutIds.AddRange(ids);
}
},
path =>
{
_logger.LogError("This is a bug, something went wrong processing {Path}", path);
return Task.CompletedTask;
});
}
}
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);
@@ -119,65 +155,135 @@ namespace ErsatzTV.Core.Metadata
return unit;
}
private async Task<Seq<MediaItem>> RefreshMetadataForItems(
string ffprobePath,
Seq<MediaItem> staleMetadataMediaItems)
private Seq<string> FindAllFiles(LocalMediaSource localMediaSource)
{
var modifiedMediaItems = new List<MediaItem>();
foreach (MediaItem mediaItem in staleMetadataMediaItems)
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<Option<MediaItem>> AddMediaItem(MediaSource mediaSource, string path)
{
try
{
_logger.LogDebug("Refreshing metadata for media item {MediaItem}", mediaItem.Path);
if (await RefreshMetadata(mediaItem, ffprobePath))
var mediaItem = new MediaItem
{
// only queue playout rebuilds for media items
// where the duration or collections have changed
modifiedMediaItems.Add(mediaItem);
}
MediaSourceId = mediaSource.Id,
Path = path,
LastWriteTime = File.GetLastWriteTimeUtc(path)
};
await _mediaItemRepository.Add(mediaItem);
return 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)
catch (Exception ex)
{
_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);
_logger.LogError(ex, "Failed to add media item for {Path}", path);
return None;
}
}
private async Task RemoveMissingItems(Seq<MediaItem> removedMediaItems)
private async Task RemoveMissingItem(MediaItem mediaItem)
{
// TODO: flag as missing? delete after some period of time?
foreach (MediaItem mediaItem in removedMediaItems)
try
{
_logger.LogDebug("Removing missing local media item {MediaItem}", mediaItem.Path);
await _mediaItemRepository.Delete(mediaItem.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to remove missing local media item {MediaItem}", mediaItem.Path);
}
}
private async Task<bool> RefreshMetadata(MediaItem mediaItem, string ffprobePath)
private async Task SavePosterForItem(MediaItem mediaItem, string posterPath)
{
bool durationChange = await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
await _localMetadataProvider.RefreshMetadata(mediaItem);
bool collectionChange = await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
return durationChange || collectionChange;
try
{
byte[] originalBytes = await File.ReadAllBytesAsync(posterPath);
Either<BaseError, string> maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null);
await maybeHash.Match(
hash =>
{
mediaItem.Poster = hash;
mediaItem.PosterLastWriteTime = File.GetLastWriteTimeUtc(posterPath);
return _mediaItemRepository.Update(mediaItem);
},
error =>
{
_logger.LogWarning(
"Unable to save poster to disk from {Path}: {Error}",
posterPath,
error.Value);
return Task.CompletedTask;
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh poster for media item {MediaItem}", mediaItem.Path);
}
}
private async Task<bool> RefreshStatisticsForItem(MediaItem mediaItem, string ffprobePath)
{
try
{
return await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh statistics for media item {MediaItem}", mediaItem.Path);
return false;
}
}
private async Task<bool> RefreshCollectionsForItem(MediaItem mediaItem)
{
try
{
return await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh collections for media item {MediaItem}", mediaItem.Path);
return false;
}
}
private async Task RefreshSidecarMetadataForItem(MediaItem mediaItem, string path)
{
try
{
await _localMetadataProvider.RefreshSidecarMetadata(mediaItem, path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh nfo metadata for media item {MediaItem}", mediaItem.Path);
}
}
private async Task RefreshFallbackMetadataForItem(MediaItem mediaItem)
{
try
{
await _localMetadataProvider.RefreshFallbackMetadata(mediaItem);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh fallback metadata for media item {MediaItem}", mediaItem.Path);
}
}
}
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Core.Metadata
{
public record LocalMediaSourcePlan(Either<string, MediaItem> Source, List<ActionPlan> ActionPlans)
{
public Either<string, MediaItem> Source { get; set; } = Source;
}
}
@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata
{
// TODO: this needs a better name
public class LocalMediaSourcePlanner : ILocalMediaSourcePlanner
{
private static readonly Seq<string> ImageFileExtensions = Seq("jpg", "jpeg", "png", "gif", "tbn");
private readonly ILocalFileSystem _localFileSystem;
public LocalMediaSourcePlanner(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
public Seq<LocalMediaSourcePlan> DetermineActions(
MediaType mediaType,
Seq<MediaItem> mediaItems,
Seq<string> files)
{
var results = new IntermediateResults();
Seq<string> videoFiles = files.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))
.Filter(f => !IsExtra(f));
(Seq<string> newFiles, Seq<MediaItem> existingMediaItems) = videoFiles.Map(
s => mediaItems.Find(i => i.Path == s).ToEither(s))
.Partition();
// new files
foreach (string file in newFiles)
{
results.Add(file, new ActionPlan(file, ScanningAction.Add));
results.Add(file, new ActionPlan(file, ScanningAction.Statistics));
Option<string> maybeNfoFile = LocateNfoFile(mediaType, files, file);
maybeNfoFile.BiIter(
nfoFile =>
{
results.Add(file, new ActionPlan(nfoFile, ScanningAction.SidecarMetadata));
results.Add(file, new ActionPlan(nfoFile, ScanningAction.Collections));
},
() =>
{
results.Add(file, new ActionPlan(file, ScanningAction.FallbackMetadata));
results.Add(file, new ActionPlan(file, ScanningAction.Collections));
});
Option<string> maybePoster = LocatePoster(mediaType, files, file);
maybePoster.IfSome(
posterFile => results.Add(file, new ActionPlan(posterFile, ScanningAction.Poster)));
}
// existing media items
foreach (MediaItem mediaItem in existingMediaItems)
{
if ((mediaItem.LastWriteTime ?? DateTime.MinValue) < _localFileSystem.GetLastWriteTime(mediaItem.Path))
{
results.Add(mediaItem, new ActionPlan(mediaItem.Path, ScanningAction.Statistics));
}
Option<string> maybeNfoFile = LocateNfoFile(mediaType, files, mediaItem.Path);
maybeNfoFile.IfSome(
nfoFile =>
{
if (mediaItem.Metadata == null || mediaItem.Metadata.Source == MetadataSource.Fallback ||
(mediaItem.Metadata.LastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(nfoFile))
{
results.Add(mediaItem, new ActionPlan(nfoFile, ScanningAction.SidecarMetadata));
results.Add(mediaItem, new ActionPlan(nfoFile, ScanningAction.Collections));
}
});
Option<string> maybePoster = LocatePoster(mediaType, files, mediaItem.Path);
maybePoster.IfSome(
posterFile =>
{
if (string.IsNullOrWhiteSpace(mediaItem.Poster) ||
(mediaItem.PosterLastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(posterFile))
{
results.Add(mediaItem, new ActionPlan(posterFile, ScanningAction.Poster));
}
});
}
// missing media items
foreach (MediaItem mediaItem in mediaItems.Where(i => !files.Contains(i.Path)))
{
results.Add(mediaItem, new ActionPlan(mediaItem.Path, ScanningAction.Remove));
}
return results.Summarize();
}
private static bool IsExtra(string path)
{
string folder = Path.GetFileName(Path.GetDirectoryName(path) ?? string.Empty);
string file = Path.GetFileNameWithoutExtension(path);
return ExtraDirectories.Contains(folder, StringComparer.OrdinalIgnoreCase)
|| ExtraFiles.Any(f => file.EndsWith(f, StringComparison.OrdinalIgnoreCase));
}
private static Option<string> LocateNfoFile(MediaType mediaType, Seq<string> files, string file)
{
switch (mediaType)
{
case MediaType.Movie:
string movieAsNfo = Path.ChangeExtension(file, "nfo");
string movieNfo = Path.Combine(Path.GetDirectoryName(file) ?? string.Empty, "movie.nfo");
return Seq(movieAsNfo, movieNfo)
.Filter(s => files.Contains(s))
.HeadOrNone();
case MediaType.TvShow:
string episodeAsNfo = Path.ChangeExtension(file, "nfo");
return Optional(episodeAsNfo)
.Filter(s => files.Contains(s))
.HeadOrNone();
}
return None;
}
private static Option<string> LocatePoster(MediaType mediaType, Seq<string> files, string file)
{
string folder = Path.GetDirectoryName(file) ?? string.Empty;
switch (mediaType)
{
case MediaType.Movie:
IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect(
ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(file) + $"-poster.{ext}" })
.Map(f => Path.Combine(folder, f));
return possibleMoviePosters.Filter(p => files.Contains(p)).HeadOrNone();
case MediaType.TvShow:
string parentFolder = Directory.GetParent(folder)?.FullName ?? string.Empty;
IEnumerable<string> possibleTvPosters = ImageFileExtensions
.Collect(ext => new[] { $"poster.{ext}" })
.Map(f => Path.Combine(parentFolder, f));
return possibleTvPosters.Filter(p => files.Contains(p)).HeadOrNone();
}
return None;
}
private class IntermediateResults
{
private readonly List<Tuple<Either<string, MediaItem>, ActionPlan>> _rawResults = new();
public void Add(Either<string, MediaItem> source, ActionPlan plan) =>
_rawResults.Add(Tuple(source, plan));
public Seq<LocalMediaSourcePlan> Summarize() =>
_rawResults
.GroupBy(t => t.Item1)
.Select(g => new LocalMediaSourcePlan(g.Key, g.Select(g2 => g2.Item2).ToList()))
.ToSeq();
}
// @formatter:off
private static readonly Seq<string> VideoFileExtensions = Seq(
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts");
private static readonly Seq<string> ExtraDirectories = Seq(
"behind the scenes", "deleted scenes", "featurettes",
"interviews", "scenes", "shorts", "trailers", "other",
"extras", "specials");
private static readonly Seq<string> ExtraFiles = Seq(
"behindthescenes", "deleted", "featurette",
"interview", "scene", "short", "trailer", "other");
// @formatter:on
}
}
@@ -25,14 +25,15 @@ namespace ErsatzTV.Core.Metadata
_logger = logger;
}
public async Task RefreshMetadata(MediaItem mediaItem)
public async Task RefreshSidecarMetadata(MediaItem mediaItem, string path)
{
Option<MediaMetadata> maybeMetadata = await LoadMetadata(mediaItem);
MediaMetadata metadata =
maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem));
await ApplyMetadataUpdate(mediaItem, metadata);
Option<MediaMetadata> maybeMetadata = await LoadMetadata(mediaItem, path);
await maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(mediaItem, metadata));
}
public Task RefreshFallbackMetadata(MediaItem mediaItem) =>
ApplyMetadataUpdate(mediaItem, FallbackMetadataProvider.GetFallbackMetadata(mediaItem));
private async Task ApplyMetadataUpdate(MediaItem mediaItem, MediaMetadata metadata)
{
if (mediaItem.Metadata == null)
@@ -40,6 +41,8 @@ namespace ErsatzTV.Core.Metadata
mediaItem.Metadata = new MediaMetadata();
}
mediaItem.Metadata.Source = metadata.Source;
mediaItem.Metadata.LastWriteTime = metadata.LastWriteTime;
mediaItem.Metadata.MediaType = metadata.MediaType;
mediaItem.Metadata.Title = metadata.Title;
mediaItem.Metadata.Subtitle = metadata.Subtitle;
@@ -56,9 +59,8 @@ namespace ErsatzTV.Core.Metadata
await _mediaItemRepository.Update(mediaItem);
}
private async Task<Option<MediaMetadata>> LoadMetadata(MediaItem mediaItem)
private async Task<Option<MediaMetadata>> LoadMetadata(MediaItem mediaItem, string nfoFileName)
{
string nfoFileName = Path.ChangeExtension(mediaItem.Path, "nfo");
if (nfoFileName == null || !File.Exists(nfoFileName))
{
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
@@ -88,6 +90,8 @@ namespace ErsatzTV.Core.Metadata
return maybeNfo.Match<Option<MediaMetadata>>(
nfo => new MediaMetadata
{
Source = MetadataSource.Sidecar,
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
MediaType = MediaType.TvShow,
Title = nfo.ShowTitle,
Subtitle = nfo.Title,
@@ -114,6 +118,8 @@ namespace ErsatzTV.Core.Metadata
return maybeNfo.Match<Option<MediaMetadata>>(
nfo => new MediaMetadata
{
Source = MetadataSource.Sidecar,
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
MediaType = MediaType.Movie,
Title = nfo.Title,
Description = nfo.Outline,
@@ -73,7 +73,7 @@ namespace ErsatzTV.Core.Metadata
return None;
}
private async Task SavePosterToDisk(MediaItem mediaItem, string posterPath)
public async Task SavePosterToDisk(MediaItem mediaItem, string posterPath)
{
byte[] originalBytes = await File.ReadAllBytesAsync(posterPath);
Either<BaseError, string> maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null);
+14
View File
@@ -0,0 +1,14 @@
namespace ErsatzTV.Core.Metadata
{
public enum ScanningAction
{
None = 0,
Add = 1,
Remove = 2,
Statistics = 3,
SidecarMetadata = 4,
FallbackMetadata = 5,
Collections = 6,
Poster = 7
}
}