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.
189 lines
7.5 KiB
189 lines
7.5 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Threading.Tasks; |
|
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Errors; |
|
using ErsatzTV.Core.Interfaces.Images; |
|
using ErsatzTV.Core.Interfaces.Metadata; |
|
using ErsatzTV.Core.Interfaces.Repositories; |
|
using LanguageExt; |
|
using Microsoft.Extensions.Logging; |
|
using static LanguageExt.Prelude; |
|
using Seq = LanguageExt.Seq; |
|
|
|
namespace ErsatzTV.Core.Metadata |
|
{ |
|
public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner |
|
{ |
|
private readonly ILocalFileSystem _localFileSystem; |
|
private readonly ILocalMetadataProvider _localMetadataProvider; |
|
private readonly ILogger<MovieFolderScanner> _logger; |
|
private readonly IMovieRepository _movieRepository; |
|
|
|
public MovieFolderScanner( |
|
ILocalFileSystem localFileSystem, |
|
IMovieRepository movieRepository, |
|
ILocalStatisticsProvider localStatisticsProvider, |
|
ILocalMetadataProvider localMetadataProvider, |
|
IImageCache imageCache, |
|
ILogger<MovieFolderScanner> logger) |
|
: base(localFileSystem, localStatisticsProvider, imageCache, logger) |
|
{ |
|
_localFileSystem = localFileSystem; |
|
_movieRepository = movieRepository; |
|
_localMetadataProvider = localMetadataProvider; |
|
_logger = logger; |
|
} |
|
|
|
public async Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath) |
|
{ |
|
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) |
|
{ |
|
return new MediaSourceInaccessible(); |
|
} |
|
|
|
var folderQueue = new Queue<string>(); |
|
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) |
|
{ |
|
folderQueue.Enqueue(folder); |
|
} |
|
|
|
while (folderQueue.Count > 0) |
|
{ |
|
string movieFolder = folderQueue.Dequeue(); |
|
|
|
var allFiles = _localFileSystem.ListFiles(movieFolder) |
|
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) |
|
.Filter( |
|
f => !ExtraFiles.Any( |
|
e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase))) |
|
.ToList(); |
|
|
|
if (allFiles.Count == 0) |
|
{ |
|
foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) |
|
{ |
|
folderQueue.Enqueue(subdirectory); |
|
} |
|
|
|
continue; |
|
} |
|
|
|
|
|
foreach (string file in allFiles.OrderBy(identity)) |
|
{ |
|
// TODO: optimize dbcontext use here, do we need tracking? can we make partial updates with dapper? |
|
// TODO: figure out how to rebuild playlists |
|
Either<BaseError, Movie> maybeMovie = await _movieRepository |
|
.GetOrAdd(libraryPath, file) |
|
.BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie)) |
|
.BindT(UpdateMetadata) |
|
.BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster)) |
|
.BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt)); |
|
|
|
maybeMovie.IfLeft( |
|
error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value)); |
|
} |
|
} |
|
|
|
foreach (string path in await _movieRepository.FindMoviePaths(libraryPath)) |
|
{ |
|
if (!_localFileSystem.FileExists(path)) |
|
{ |
|
_logger.LogInformation("Removing missing movie at {Path}", path); |
|
await _movieRepository.DeleteByPath(libraryPath, path); |
|
} |
|
} |
|
|
|
return Unit.Default; |
|
} |
|
|
|
private async Task<Either<BaseError, Movie>> UpdateMetadata(Movie movie) |
|
{ |
|
try |
|
{ |
|
await LocateNfoFile(movie).Match( |
|
async nfoFile => |
|
{ |
|
bool shouldUpdate = Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match( |
|
m => m.MetadataKind == MetadataKind.Fallback || |
|
m.DateUpdated < _localFileSystem.GetLastWriteTime(nfoFile), |
|
true); |
|
|
|
if (shouldUpdate) |
|
{ |
|
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); |
|
await _localMetadataProvider.RefreshSidecarMetadata(movie, nfoFile); |
|
} |
|
}, |
|
async () => |
|
{ |
|
if (!Optional(movie.MovieMetadata).Flatten().Any()) |
|
{ |
|
string path = movie.MediaVersions.Head().MediaFiles.Head().Path; |
|
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); |
|
await _localMetadataProvider.RefreshFallbackMetadata(movie); |
|
} |
|
}); |
|
|
|
return movie; |
|
} |
|
catch (Exception ex) |
|
{ |
|
return BaseError.New(ex.Message); |
|
} |
|
} |
|
|
|
private async Task<Either<BaseError, Movie>> UpdateArtwork(Movie movie, ArtworkKind artworkKind) |
|
{ |
|
try |
|
{ |
|
await LocateArtwork(movie, artworkKind).IfSomeAsync( |
|
async posterFile => |
|
{ |
|
MovieMetadata metadata = movie.MovieMetadata.Head(); |
|
if (RefreshArtwork(posterFile, metadata, artworkKind)) |
|
{ |
|
await _movieRepository.Update(movie); |
|
} |
|
}); |
|
|
|
return movie; |
|
} |
|
catch (Exception ex) |
|
{ |
|
return BaseError.New(ex.Message); |
|
} |
|
} |
|
|
|
private Option<string> LocateNfoFile(Movie movie) |
|
{ |
|
string path = movie.MediaVersions.Head().MediaFiles.Head().Path; |
|
string movieAsNfo = Path.ChangeExtension(path, "nfo"); |
|
string movieNfo = Path.Combine(Path.GetDirectoryName(path) ?? string.Empty, "movie.nfo"); |
|
return Seq.create(movieAsNfo, movieNfo) |
|
.Filter(s => _localFileSystem.FileExists(s)) |
|
.HeadOrNone(); |
|
} |
|
|
|
private Option<string> LocateArtwork(Movie movie, ArtworkKind artworkKind) |
|
{ |
|
string segment = artworkKind switch |
|
{ |
|
ArtworkKind.Poster => "poster", |
|
ArtworkKind.FanArt => "fanart", |
|
_ => throw new ArgumentOutOfRangeException(nameof(artworkKind)) |
|
}; |
|
|
|
string path = movie.MediaVersions.Head().MediaFiles.Head().Path; |
|
string folder = Path.GetDirectoryName(path) ?? string.Empty; |
|
IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect( |
|
ext => new[] { $"{segment}.{ext}", Path.GetFileNameWithoutExtension(path) + $"-{segment}.{ext}" }) |
|
.Map(f => Path.Combine(folder, f)); |
|
Option<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone(); |
|
return result; |
|
} |
|
} |
|
}
|
|
|