From 8fea24a3a53885153a9e9e93e57d62ae4d7cef5a Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 4 Apr 2021 15:57:25 -0500 Subject: [PATCH] add fallback metadata for music videos (#134) --- .../Metadata/IFallbackMetadataProvider.cs | 1 + .../Metadata/ILocalMetadataProvider.cs | 3 +- .../Repositories/IMusicVideoRepository.cs | 8 +- .../Metadata/FallbackMetadataProvider.cs | 37 +++++ .../Metadata/LocalMetadataProvider.cs | 22 +-- .../Metadata/MusicVideoFolderScanner.cs | 71 ++++----- .../Data/Repositories/MusicVideoRepository.cs | 135 ++++++++---------- 7 files changed, 130 insertions(+), 147 deletions(-) diff --git a/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs index 64121a0b6..1d76f79d3 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs @@ -8,6 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata ShowMetadata GetFallbackMetadataForShow(string showFolder); Tuple GetFallbackMetadata(Episode episode); MovieMetadata GetFallbackMetadata(Movie movie); + MusicVideoMetadata GetFallbackMetadata(MusicVideo musicVideo); string GetSortTitle(string title); } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs index c4807d643..f942c4219 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -1,19 +1,18 @@ using System.Threading.Tasks; using ErsatzTV.Core.Domain; -using LanguageExt; namespace ErsatzTV.Core.Interfaces.Metadata { public interface ILocalMetadataProvider { Task GetMetadataForShow(string showFolder); - Task> GetMetadataForMusicVideo(string filePath); Task RefreshSidecarMetadata(Movie movie, string nfoFileName); Task RefreshSidecarMetadata(Show televisionShow, string nfoFileName); Task RefreshSidecarMetadata(Episode episode, string nfoFileName); Task RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName); Task RefreshFallbackMetadata(Movie movie); Task RefreshFallbackMetadata(Episode episode); + Task RefreshFallbackMetadata(MusicVideo musicVideo); Task RefreshFallbackMetadata(Show televisionShow, string showFolder); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs index 2450ee750..c87e098db 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs @@ -8,13 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IMusicVideoRepository { - Task> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata); - - Task>> Add( - LibraryPath libraryPath, - string filePath, - MusicVideoMetadata metadata); - + Task>> GetOrAdd(LibraryPath libraryPath, string path); Task> FindMusicVideoPaths(LibraryPath libraryPath); Task> DeleteByPath(LibraryPath libraryPath, string path); Task AddGenre(MusicVideoMetadata metadata, Genre genre); diff --git a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs index a6a91e421..52a1d37d9 100644 --- a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs @@ -36,6 +36,19 @@ namespace ErsatzTV.Core.Metadata return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata; } + public MusicVideoMetadata GetFallbackMetadata(MusicVideo musicVideo) + { + string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path; + string fileName = Path.GetFileName(path); + var metadata = new MusicVideoMetadata + { + MetadataKind = MetadataKind.Fallback, + Title = fileName ?? path + }; + + return fileName != null ? GetMusicVideoMetadata(fileName, metadata) : metadata; + } + public string GetSortTitle(string title) { if (string.IsNullOrWhiteSpace(title)) @@ -112,6 +125,30 @@ namespace ErsatzTV.Core.Metadata return metadata; } + private MusicVideoMetadata GetMusicVideoMetadata(string fileName, MusicVideoMetadata metadata) + { + try + { + const string PATTERN = @"^(.*?) - (.*?).\w+$"; + Match match = Regex.Match(fileName, PATTERN); + if (match.Success) + { + metadata.Artist = match.Groups[1].Value; + metadata.Title = match.Groups[2].Value; + metadata.Genres = new List(); + metadata.Tags = new List(); + metadata.Studios = new List(); + metadata.DateUpdated = DateTime.UtcNow; + } + } + catch (Exception) + { + // ignored + } + + return metadata; + } + private ShowMetadata GetTelevisionShowMetadata(string fileName, ShowMetadata metadata) { try diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index b465f10d4..62cd42fa0 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -70,23 +70,6 @@ namespace ErsatzTV.Core.Metadata }); } - public async Task> GetMetadataForMusicVideo(string filePath) - { - string nfoFileName = Path.ChangeExtension(filePath, "nfo"); - Option maybeMetadata = None; - if (_localFileSystem.FileExists(nfoFileName)) - { - maybeMetadata = await LoadMusicVideoMetadata(nfoFileName); - } - - return maybeMetadata.Map( - metadata => - { - metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title); - return metadata; - }); - } - public Task RefreshSidecarMetadata(Movie movie, string nfoFileName) => LoadMovieMetadata(movie, nfoFileName).Bind( maybeMetadata => maybeMetadata.Match( @@ -109,7 +92,7 @@ namespace ErsatzTV.Core.Metadata LoadMusicVideoMetadata(nfoFileName).Bind( maybeMetadata => maybeMetadata.Match( metadata => ApplyMetadataUpdate(musicVideo, metadata), - () => Task.FromResult(false))); + () => RefreshFallbackMetadata(musicVideo))); public Task RefreshFallbackMetadata(Movie movie) => ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie)); @@ -117,6 +100,9 @@ namespace ErsatzTV.Core.Metadata public Task RefreshFallbackMetadata(Episode episode) => ApplyMetadataUpdate(episode, _fallbackMetadataProvider.GetFallbackMetadata(episode)); + public Task RefreshFallbackMetadata(MusicVideo musicVideo) => + ApplyMetadataUpdate(musicVideo, _fallbackMetadataProvider.GetFallbackMetadata(musicVideo)); + public Task RefreshFallbackMetadata(Show televisionShow, string showFolder) => ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder)); diff --git a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs index d264f8ec0..4a7449682 100644 --- a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs @@ -67,10 +67,7 @@ namespace ErsatzTV.Core.Metadata var foldersCompleted = 0; var folderQueue = new Queue(); - foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) - { - folderQueue.Enqueue(folder); - } + folderQueue.Enqueue(libraryPath.Path); while (folderQueue.Count > 0) { @@ -83,20 +80,13 @@ namespace ErsatzTV.Core.Metadata var allFiles = _localFileSystem.ListFiles(musicVideoFolder) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) - .Filter( - f => !ExtraFiles.Any( - e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase))) + .Filter(f => f.Contains(" - ")) .ToList(); - if (allFiles.Count == 0) + foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder) + .OrderBy(identity)) { - foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder) - .OrderBy(identity)) - { - folderQueue.Enqueue(subdirectory); - } - - continue; + folderQueue.Enqueue(subdirectory); } if (_localFileSystem.GetLastWriteTime(musicVideoFolder) < lastScan) @@ -107,11 +97,11 @@ namespace ErsatzTV.Core.Metadata foreach (string file in allFiles.OrderBy(identity)) { // TODO: figure out how to rebuild playouts - Either> maybeMusicVideo = - await FindOrCreateMusicVideo(libraryPath, file) - .BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath)) - .BindT(UpdateMetadata) - .BindT(UpdateThumbnail); + Either> maybeMusicVideo = await _musicVideoRepository + .GetOrAdd(libraryPath, file) + .BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath)) + .BindT(UpdateMetadata) + .BindT(UpdateThumbnail); await maybeMusicVideo.Match( async result => @@ -146,34 +136,13 @@ namespace ErsatzTV.Core.Metadata return Unit.Default; } - private async Task>> FindOrCreateMusicVideo( - LibraryPath libraryPath, - string filePath) - { - Option maybeMetadata = await _localMetadataProvider.GetMetadataForMusicVideo(filePath); - return await maybeMetadata.Match( - async metadata => - { - Option maybeMusicVideo = - await _musicVideoRepository.GetByMetadata(libraryPath, metadata); - return await maybeMusicVideo.Match( - musicVideo => - Right>( - new MediaItemScanResult(musicVideo)) - .AsTask(), - async () => await _musicVideoRepository.Add(libraryPath, filePath, metadata)); - }, - () => Left>( - BaseError.New("Unable to locate metadata for music video")).AsTask()); - } - private async Task>> UpdateMetadata( MediaItemScanResult result) { try { MusicVideo musicVideo = result.Item; - return await LocateNfoFile(musicVideo).Match>>>( + await LocateNfoFile(musicVideo).Match( async nfoFile => { bool shouldUpdate = Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match( @@ -189,11 +158,21 @@ namespace ErsatzTV.Core.Metadata result.IsUpdated = true; } } - - return result; }, - () => Left>( - BaseError.New("Unable to locate metadata for music video")).AsTask()); + async () => + { + if (!Optional(musicVideo.MusicVideoMetadata).Flatten().Any()) + { + string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path; + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); + if (await _localMetadataProvider.RefreshFallbackMetadata(musicVideo)) + { + result.IsUpdated = true; + } + } + }); + + return result; } catch (Exception ex) { diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs index e8b60695c..5181ac545 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs @@ -25,83 +25,35 @@ namespace ErsatzTV.Infrastructure.Data.Repositories _dbConnection = dbConnection; } - public async Task> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Option maybeId = await dbContext.MusicVideoMetadata - .Where(s => s.Artist == metadata.Artist && s.Title == metadata.Title && s.Year == metadata.Year) - .Where(s => s.MusicVideo.LibraryPathId == libraryPath.Id) - .SingleOrDefaultAsync() - .Map(Optional) - .MapT(sm => sm.MusicVideoId); - - return await maybeId.Match( - id => - { - return dbContext.MusicVideos - .AsNoTracking() - .Include(mv => mv.MusicVideoMetadata) - .ThenInclude(mvm => mvm.Artwork) - .Include(mv => mv.MusicVideoMetadata) - .ThenInclude(mvm => mvm.Genres) - .Include(mv => mv.MusicVideoMetadata) - .ThenInclude(mvm => mvm.Tags) - .Include(mv => mv.MusicVideoMetadata) - .ThenInclude(mvm => mvm.Studios) - .Include(mv => mv.LibraryPath) - .ThenInclude(lp => lp.Library) - .Include(mv => mv.MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .Include(mv => mv.MediaVersions) - .ThenInclude(mv => mv.Streams) - .OrderBy(mv => mv.Id) - .SingleOrDefaultAsync(mv => mv.Id == id) - .Map(Optional); - }, - () => Option.None.AsTask()); - } - - public async Task>> Add( + public async Task>> GetOrAdd( LibraryPath libraryPath, - string filePath, - MusicVideoMetadata metadata) + string path) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - try - { - metadata.DateAdded = DateTime.UtcNow; - metadata.Genres ??= new List(); - metadata.Tags ??= new List(); - metadata.Studios ??= new List(); - var musicVideo = new MusicVideo - { - LibraryPathId = libraryPath.Id, - MusicVideoMetadata = new List { metadata }, - MediaVersions = new List - { - new() - { - MediaFiles = new List - { - new() { Path = filePath } - }, - Streams = new List() - } - } - }; - - await dbContext.MusicVideos.AddAsync(musicVideo); - await dbContext.SaveChangesAsync(); - await dbContext.Entry(musicVideo).Reference(s => s.LibraryPath).LoadAsync(); - await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync(); - - return new MediaItemScanResult(musicVideo) { IsAdded = true }; - } - catch (Exception ex) - { - return BaseError.New(ex.Message); - } + Option maybeExisting = await dbContext.MusicVideos + .AsNoTracking() + .Include(mv => mv.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Artwork) + .Include(mv => mv.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Genres) + .Include(mv => mv.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Tags) + .Include(mv => mv.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Studios) + .Include(mv => mv.LibraryPath) + .ThenInclude(lp => lp.Library) + .Include(mv => mv.MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(mv => mv.MediaVersions) + .ThenInclude(mv => mv.Streams) + .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path) + .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path); + + return await maybeExisting.Match( + mediaItem => + Right>( + new MediaItemScanResult(mediaItem) { IsAdded = false }).AsTask(), + async () => await AddMusicVideo(dbContext, libraryPath.Id, path)); } public Task> FindMusicVideoPaths(LibraryPath libraryPath) => @@ -179,5 +131,40 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .SingleOrDefaultAsync(m => m.Id == musicVideoId) .Map(Optional); } + + private static async Task>> AddMusicVideo( + TvContext dbContext, + int libraryPathId, + string path) + { + try + { + var musicVideo = new MusicVideo + { + LibraryPathId = libraryPathId, + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = path } + }, + Streams = new List() + } + } + }; + + await dbContext.MusicVideos.AddAsync(musicVideo); + await dbContext.SaveChangesAsync(); + await dbContext.Entry(musicVideo).Reference(m => m.LibraryPath).LoadAsync(); + await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync(); + return new MediaItemScanResult(musicVideo) { IsAdded = true }; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } } }