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.
311 lines
12 KiB
311 lines
12 KiB
using System.Collections.Immutable; |
|
using Bugsnag; |
|
using CliWrap; |
|
using ErsatzTV.Core; |
|
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Extensions; |
|
using ErsatzTV.Core.FFmpeg; |
|
using ErsatzTV.Core.Interfaces.FFmpeg; |
|
using ErsatzTV.Core.Interfaces.Images; |
|
using ErsatzTV.Core.Interfaces.Metadata; |
|
using ErsatzTV.Core.Interfaces.Repositories; |
|
using ErsatzTV.Core.Metadata; |
|
using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; |
|
using ErsatzTV.Scanner.Core.Interfaces.Metadata; |
|
using Microsoft.Extensions.Logging; |
|
|
|
namespace ErsatzTV.Scanner.Core.Metadata; |
|
|
|
public abstract class LocalFolderScanner |
|
{ |
|
public static readonly ImmutableHashSet<string> VideoFileExtensions = new[] |
|
{ |
|
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", |
|
".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".m2ts", ".ts", ".webm" |
|
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); |
|
|
|
public static readonly ImmutableHashSet<string> AudioFileExtensions = new[] |
|
{ |
|
".aac", ".alac", ".dff", ".dsf", ".flac", ".mp3", ".m4a", ".ogg", ".opus", ".oga", ".ogx", ".spx", ".wav", |
|
".wma" |
|
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); |
|
|
|
public static readonly ImmutableHashSet<string> ImageFileExtensions = new[] |
|
{ |
|
"jpg", "jpeg", "png", "gif", "tbn", "webp" |
|
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); |
|
|
|
public static readonly ImmutableHashSet<string> ExtraFiles = new[] |
|
{ |
|
"behindthescenes", "deleted", "featurette", |
|
"interview", "scene", "short", "trailer", "other" |
|
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); |
|
|
|
public static readonly ImmutableHashSet<string> ExtraDirectories = new List<string> |
|
{ |
|
"behind the scenes", "deleted scenes", "featurettes", |
|
"interviews", "scenes", "shorts", "trailers", "other", |
|
"extras", "specials" |
|
} |
|
.Map(s => $"{Path.DirectorySeparatorChar}{s}{Path.DirectorySeparatorChar}") |
|
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); |
|
|
|
private readonly IClient _client; |
|
private readonly IFFmpegPngService _ffmpegPngService; |
|
|
|
private readonly IImageCache _imageCache; |
|
|
|
private readonly ILocalFileSystem _localFileSystem; |
|
private readonly ILocalStatisticsProvider _localStatisticsProvider; |
|
private readonly ILogger _logger; |
|
private readonly IMediaItemRepository _mediaItemRepository; |
|
private readonly IMetadataRepository _metadataRepository; |
|
private readonly ITempFilePool _tempFilePool; |
|
|
|
protected LocalFolderScanner( |
|
ILocalFileSystem localFileSystem, |
|
ILocalStatisticsProvider localStatisticsProvider, |
|
IMetadataRepository metadataRepository, |
|
IMediaItemRepository mediaItemRepository, |
|
IImageCache imageCache, |
|
IFFmpegPngService ffmpegPngService, |
|
ITempFilePool tempFilePool, |
|
IClient client, |
|
ILogger logger) |
|
{ |
|
_localFileSystem = localFileSystem; |
|
_localStatisticsProvider = localStatisticsProvider; |
|
_metadataRepository = metadataRepository; |
|
_mediaItemRepository = mediaItemRepository; |
|
_imageCache = imageCache; |
|
_ffmpegPngService = ffmpegPngService; |
|
_tempFilePool = tempFilePool; |
|
_client = client; |
|
_logger = logger; |
|
} |
|
|
|
protected async Task<Either<BaseError, MediaItemScanResult<T>>> UpdateStatistics<T>( |
|
MediaItemScanResult<T> mediaItem, |
|
string ffmpegPath, |
|
string ffprobePath) |
|
where T : MediaItem |
|
{ |
|
try |
|
{ |
|
MediaVersion version = mediaItem.Item.GetHeadVersion(); |
|
|
|
string path = version.MediaFiles.Head().Path; |
|
|
|
if (version.DateUpdated != _localFileSystem.GetLastWriteTime(path) || version.Streams.Count == 0) |
|
{ |
|
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path); |
|
Either<BaseError, bool> refreshResult = |
|
await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, mediaItem.Item); |
|
|
|
foreach (BaseError error in refreshResult.LeftToSeq()) |
|
{ |
|
_logger.LogWarning( |
|
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}", |
|
"Statistics", |
|
path, |
|
error.Value); |
|
} |
|
|
|
foreach (bool result in refreshResult.RightToSeq()) |
|
{ |
|
if (result) |
|
{ |
|
mediaItem.IsUpdated = true; |
|
} |
|
} |
|
} |
|
|
|
return mediaItem; |
|
} |
|
catch (Exception ex) |
|
{ |
|
_client.Notify(ex); |
|
return BaseError.New(ex.Message); |
|
} |
|
} |
|
|
|
protected async Task<bool> RefreshArtwork( |
|
string artworkFile, |
|
ErsatzTV.Core.Domain.Metadata metadata, |
|
ArtworkKind artworkKind, |
|
Option<string> ffmpegPath, |
|
Option<int> attachedPicIndex, |
|
CancellationToken cancellationToken) |
|
{ |
|
DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile); |
|
|
|
metadata.Artwork ??= new List<Artwork>(); |
|
|
|
Option<Artwork> maybeArtwork = metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind); |
|
|
|
bool shouldRefresh = maybeArtwork.Match( |
|
artwork => lastWriteTime.Subtract(artwork.DateUpdated) > TimeSpan.FromSeconds(1), |
|
true); |
|
|
|
if (shouldRefresh) |
|
{ |
|
try |
|
{ |
|
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile); |
|
|
|
string sourcePath = artworkFile; |
|
if (await _metadataRepository.CloneArtwork( |
|
metadata, |
|
maybeArtwork, |
|
artworkKind, |
|
sourcePath, |
|
lastWriteTime)) |
|
{ |
|
return true; |
|
} |
|
|
|
// if ffmpeg path is passed, we need pre-processing |
|
foreach (string path in ffmpegPath) |
|
{ |
|
artworkFile = await attachedPicIndex.Match( |
|
async picIndex => |
|
{ |
|
// extract attached pic (and convert to png) |
|
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt); |
|
Command process = _ffmpegPngService.ExtractAttachedPicAsPng( |
|
path, |
|
artworkFile, |
|
picIndex, |
|
tempName); |
|
|
|
await process.ExecuteAsync(cancellationToken); |
|
|
|
return tempName; |
|
}, |
|
async () => |
|
{ |
|
// no attached pic index means convert to png |
|
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt); |
|
Command process = _ffmpegPngService.ConvertToPng(path, artworkFile, tempName); |
|
|
|
await process.ExecuteAsync(cancellationToken); |
|
|
|
return tempName; |
|
}); |
|
} |
|
|
|
Either<BaseError, string> maybeCacheName = |
|
await _imageCache.CopyArtworkToCache(artworkFile, artworkKind); |
|
|
|
return await maybeCacheName.Match( |
|
async cacheName => |
|
{ |
|
await maybeArtwork.Match( |
|
async artwork => |
|
{ |
|
artwork.Path = cacheName; |
|
artwork.SourcePath = sourcePath; |
|
artwork.DateUpdated = lastWriteTime; |
|
|
|
if (metadata is SongMetadata) |
|
{ |
|
artwork.BlurHash43 = await _imageCache.CalculateBlurHash( |
|
cacheName, |
|
artworkKind, |
|
4, |
|
3); |
|
artwork.BlurHash54 = await _imageCache.CalculateBlurHash( |
|
cacheName, |
|
artworkKind, |
|
5, |
|
4); |
|
artwork.BlurHash64 = await _imageCache.CalculateBlurHash( |
|
cacheName, |
|
artworkKind, |
|
6, |
|
4); |
|
} |
|
|
|
await _metadataRepository.UpdateArtworkPath(artwork); |
|
}, |
|
async () => |
|
{ |
|
var artwork = new Artwork |
|
{ |
|
Path = cacheName, |
|
SourcePath = sourcePath, |
|
DateAdded = DateTime.UtcNow, |
|
DateUpdated = lastWriteTime, |
|
ArtworkKind = artworkKind |
|
}; |
|
|
|
if (metadata is SongMetadata) |
|
{ |
|
artwork.BlurHash43 = await _imageCache.CalculateBlurHash( |
|
cacheName, |
|
artworkKind, |
|
4, |
|
3); |
|
artwork.BlurHash54 = await _imageCache.CalculateBlurHash( |
|
cacheName, |
|
artworkKind, |
|
5, |
|
4); |
|
artwork.BlurHash64 = await _imageCache.CalculateBlurHash( |
|
cacheName, |
|
artworkKind, |
|
6, |
|
4); |
|
} |
|
|
|
metadata.Artwork.Add(artwork); |
|
await _metadataRepository.AddArtwork(metadata, artwork); |
|
}); |
|
|
|
return true; |
|
}, |
|
error => |
|
{ |
|
_logger.LogDebug("Failed to cache artwork from {Path}: {Error}", artworkFile, error.Value); |
|
return Task.FromResult(false); |
|
}); |
|
} |
|
catch (Exception ex) |
|
{ |
|
_logger.LogWarning(ex, "Error refreshing artwork"); |
|
_client.Notify(ex); |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
protected Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path) => |
|
_mediaItemRepository.FlagFileNotFound(libraryPath, path); |
|
|
|
protected async Task<Either<BaseError, MediaItemScanResult<T>>> FlagNormal<T>(MediaItemScanResult<T> result) |
|
where T : MediaItem |
|
{ |
|
try |
|
{ |
|
T mediaItem = result.Item; |
|
if (mediaItem.State != MediaItemState.Normal) |
|
{ |
|
await _mediaItemRepository.FlagNormal(mediaItem); |
|
result.IsUpdated = true; |
|
} |
|
|
|
return result; |
|
} |
|
catch (Exception ex) |
|
{ |
|
_client.Notify(ex); |
|
return BaseError.New(ex.ToString()); |
|
} |
|
} |
|
|
|
protected bool ShouldIncludeFolder(string folder) => |
|
!string.IsNullOrWhiteSpace(folder) && |
|
!Path.GetFileName(folder).StartsWith('.') && |
|
!_localFileSystem.FileExists(Path.Combine(folder, ".etvignore")); |
|
}
|
|
|