diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d31dd524..8e9dd75c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix 500 errors when serving XMLTV due to concurrent file reads and writes - Fix playback of AC3 audio when targeting stereo output and input layout changes mid-stream - Use other video artwork in XMLTV template -- Remove artwork from other videos when artwork has been removed from disk +- Properly update (add or remove) artwork for all local media libraries when files have changed ## [26.1.1] - 2026-01-08 ### Fixed diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs index bd3ec45fa..05b762a4a 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs @@ -16,7 +16,7 @@ public interface IMetadataRepository Task UpdateStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true); Task UpdateArtworkPath(Artwork artwork); Task AddArtwork(Domain.Metadata metadata, Artwork artwork); - Task RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind); + Task RemoveArtworkWithKind(Domain.Metadata metadata, ArtworkKind artworkKind); Task CloneArtwork( Domain.Metadata metadata, diff --git a/ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs index c1ee47f00..819477dfc 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs @@ -19,5 +19,4 @@ public interface IOtherVideoRepository Task AddActor(OtherVideoMetadata metadata, Actor actor); Task AddDirector(OtherVideoMetadata metadata, Director director); Task AddWriter(OtherVideoMetadata metadata, Writer writer); - Task RemoveArtwork(OtherVideoMetadata metadata, ArtworkKind artworkKind); } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IPlexMetadataRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IPlexMetadataRepository.cs new file mode 100644 index 000000000..f5669bbe4 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Repositories/IPlexMetadataRepository.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Interfaces.Repositories; + +public interface IPlexMetadataRepository +{ + Task RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind); +} diff --git a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs index 8896c0caa..93fc0862b 100644 --- a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs @@ -44,10 +44,11 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat { MetadataKind = MetadataKind.Fallback, Title = fileName ?? showFolder, - Genres = new List(), - Tags = new List(), - Studios = new List(), - Actors = new List() + Genres = [], + Tags = [], + Studios = [], + Actors = [], + Artwork = [] }; return GetTelevisionShowMetadata(fileName, metadata); } @@ -56,7 +57,11 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat { string fileName = Path.GetFileName(artistFolder); return new ArtistMetadata - { MetadataKind = MetadataKind.Fallback, Title = fileName ?? artistFolder }; + { + MetadataKind = MetadataKind.Fallback, + Title = fileName ?? artistFolder, + Artwork = [] + }; } public List GetFallbackMetadata(Episode episode) @@ -69,18 +74,18 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat Title = Path.GetFileNameWithoutExtension(path) ?? path, DateAdded = DateTime.UtcNow, EpisodeNumber = 0, - Actors = new List(), - Artwork = new List(), - Directors = new List(), - Genres = new List(), - Guids = new List(), - Studios = new List(), - Tags = new List(), - Writers = new List() + Actors = [], + Artwork = [], + Directors = [], + Genres = [], + Guids = [], + Studios = [], + Tags = [], + Writers = [] }; return fileName != null ? GetEpisodeMetadata(fileName, baseMetadata) - : new List { baseMetadata }; + : [baseMetadata]; } public MovieMetadata GetFallbackMetadata(Movie movie) @@ -91,13 +96,14 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat { MetadataKind = MetadataKind.Fallback, Title = Path.GetFileNameWithoutExtension(path) ?? path, - Genres = new List(), - Tags = new List(), - Studios = new List(), - Actors = new List(), - Directors = new List(), - Writers = new List(), - Guids = new List() + Artwork = [], + Genres = [], + Tags = [], + Studios = [], + Actors = [], + Directors = [], + Writers = [], + Guids = [] }; return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata; @@ -110,7 +116,8 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat var metadata = new MusicVideoMetadata { MetadataKind = MetadataKind.Fallback, - Title = fileName ?? path + Title = fileName ?? path, + Artwork = [] }; return GetMusicVideoMetadata(fileName, metadata); @@ -125,13 +132,14 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat MetadataKind = MetadataKind.Fallback, Title = fileName ?? path, OtherVideo = otherVideo, - Genres = new List(), - Tags = new List(), - Studios = new List(), - Actors = new List(), - Directors = new List(), - Writers = new List(), - Guids = new List() + Artwork = [], + Genres = [], + Tags = [], + Studios = [], + Actors = [], + Directors = [], + Writers = [], + Guids = [] }; return GetOtherVideoMetadata(path, metadata); @@ -145,7 +153,8 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat { MetadataKind = MetadataKind.Fallback, Title = fileName ?? path, - Song = song + Song = song, + Artwork = [] }; return GetSongMetadata(path, metadata); @@ -160,6 +169,7 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat MetadataKind = MetadataKind.Fallback, Title = fileName ?? path, Image = image, + Artwork = [], Genres = [], Tags = [], Studios = [], @@ -179,6 +189,7 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat MetadataKind = MetadataKind.Fallback, Title = fileName ?? path, RemoteStream = remoteStream, + Artwork = [], Genres = [], Tags = [], Studios = [], @@ -232,14 +243,14 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat DateAdded = baseMetadata.DateAdded, DateUpdated = baseMetadata.DateAdded, Title = baseMetadata.Title, - Actors = new List(), - Artwork = new List(), - Directors = new List(), - Genres = new List(), - Guids = new List(), - Studios = new List(), - Tags = new List(), - Writers = new List() + Actors = [], + Artwork = [], + Directors = [], + Genres = [], + Guids = [], + Studios = [], + Tags = [], + Writers = [] }; result.Add(metadata); @@ -290,6 +301,7 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat ? match.Groups[2].Value.Trim() : Path.GetFileNameWithoutExtension(fileName); metadata.Artists = []; + metadata.Artwork = []; metadata.Genres = []; metadata.Tags = []; metadata.Studios = []; @@ -326,11 +338,11 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat .Map(t => new Tag { Name = t }) .ToList(); - metadata.Artwork = new List(); - metadata.Actors = new List(); - metadata.Genres = new List(); + metadata.Actors = []; + metadata.Artwork = []; + metadata.Genres = []; metadata.Tags = tags; - metadata.Studios = new List(); + metadata.Studios = []; metadata.DateUpdated = DateTime.UtcNow; metadata.OriginalTitle = Path.GetRelativePath(libraryPath, path); @@ -364,8 +376,8 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat .Map(t => new Tag { Name = t }) .ToList(); - metadata.Artwork = []; metadata.Actors = []; + metadata.Artwork = []; metadata.Genres = []; metadata.Tags = tags; metadata.Studios = []; @@ -402,8 +414,8 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat .Map(t => new Tag { Name = t }) .ToList(); - metadata.Artwork = []; metadata.Actors = []; + metadata.Artwork = []; metadata.Genres = []; metadata.Tags = tags; metadata.Studios = []; @@ -440,11 +452,11 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat .Map(t => new Tag { Name = t }) .ToList(); - metadata.Artwork = new List(); - metadata.Actors = new List(); - metadata.Genres = new List(); + metadata.Actors = []; + metadata.Artwork = []; + metadata.Genres = []; metadata.Tags = tags; - metadata.Studios = new List(); + metadata.Studios = []; metadata.DateUpdated = DateTime.UtcNow; metadata.OriginalTitle = Path.GetRelativePath(libraryPath, path); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs index 20a401a30..d3738e187 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs @@ -309,14 +309,13 @@ public class MetadataRepository(IDbContextFactory dbContextFactory) : }; } - public async Task RemoveArtwork(Core.Domain.Metadata metadata, ArtworkKind artworkKind) + public async Task RemoveArtworkWithKind(Core.Domain.Metadata metadata, ArtworkKind artworkKind) { - // this is only used by plex, so only needs to support plex media kinds (movie, show, season, episode, other video) await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); - return await dbContext.Connection.ExecuteAsync( - @"DELETE FROM Artwork WHERE ArtworkKind = @ArtworkKind AND (MovieMetadataId = @Id - OR ShowMetadataId = @Id OR SeasonMetadataId = @Id OR EpisodeMetadataId = @Id OR OtherVideoMetadataId = @Id)", - new { ArtworkKind = artworkKind, metadata.Id }).ToUnit(); + var ids = metadata.Artwork.Where(a => a.ArtworkKind == artworkKind).Select(a => a.Id).ToHashSet(); + return await dbContext.Artwork + .Where(a => ids.Contains(a.Id)) + .ExecuteDeleteAsync() > 0; } public async Task CloneArtwork( diff --git a/ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs index 2c4135c3b..fe5bd474c 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs @@ -175,15 +175,6 @@ public class OtherVideoRepository : IOtherVideoRepository new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0); } - public async Task RemoveArtwork(OtherVideoMetadata metadata, ArtworkKind artworkKind) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); - var ids = metadata.Artwork.Where(a => a.ArtworkKind == artworkKind).Select(a => a.Id).ToHashSet(); - return await dbContext.Artwork - .Where(a => ids.Contains(a.Id)) - .ExecuteDeleteAsync() > 0; - } - private async Task>> AddOtherVideo( TvContext dbContext, int libraryPathId, diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlexMetadataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlexMetadataRepository.cs new file mode 100644 index 000000000..f94b0db1e --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlexMetadataRepository.cs @@ -0,0 +1,19 @@ +using Dapper; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Infrastructure.Data.Repositories; + +public class PlexMetadataRepository(IDbContextFactory dbContextFactory) : IPlexMetadataRepository +{ + public async Task RemoveArtwork(Core.Domain.Metadata metadata, ArtworkKind artworkKind) + { + // this is only used by plex, so only needs to support plex media kinds (movie, show, season, episode, other video) + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); + return await dbContext.Connection.ExecuteAsync( + @"DELETE FROM Artwork WHERE ArtworkKind = @ArtworkKind AND (MovieMetadataId = @Id + OR ShowMetadataId = @Id OR SeasonMetadataId = @Id OR EpisodeMetadataId = @Id OR OtherVideoMetadataId = @Id)", + new { ArtworkKind = artworkKind, metadata.Id }).ToUnit(); + } +} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs index 189fedb95..baa121eea 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs @@ -25,25 +25,27 @@ public class RemoteStreamRepository( Option maybeExisting = await dbContext.RemoteStreams .AsNoTracking() .Include(i => i.RemoteStreamMetadata) - .ThenInclude(ovm => ovm.Genres) + .ThenInclude(rsm => rsm.Genres) .Include(i => i.RemoteStreamMetadata) - .ThenInclude(ovm => ovm.Tags) + .ThenInclude(rsm => rsm.Tags) .Include(i => i.RemoteStreamMetadata) - .ThenInclude(ovm => ovm.Studios) + .ThenInclude(rsm => rsm.Studios) .Include(i => i.RemoteStreamMetadata) - .ThenInclude(ovm => ovm.Guids) + .ThenInclude(rsm => rsm.Guids) .Include(i => i.RemoteStreamMetadata) - .ThenInclude(ovm => ovm.Actors) + .ThenInclude(rsm => rsm.Actors) .Include(i => i.RemoteStreamMetadata) - .ThenInclude(ovm => ovm.Actors) + .ThenInclude(rsm => rsm.Actors) .ThenInclude(a => a.Artwork) - .Include(ov => ov.LibraryPath) + .Include(i => i.RemoteStreamMetadata) + .ThenInclude(rsm => rsm.Artwork) + .Include(rs => rs.LibraryPath) .ThenInclude(lp => lp.Library) - .Include(ov => ov.MediaVersions) - .ThenInclude(ov => ov.MediaFiles) - .Include(ov => ov.MediaVersions) - .ThenInclude(ov => ov.Streams) - .Include(ov => ov.TraktListItems) + .Include(rs => rs.MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(rs => rs.MediaVersions) + .ThenInclude(mv => mv.Streams) + .Include(rs => rs.TraktListItems) .ThenInclude(tli => tli.TraktList) .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path) .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path, cancellationToken); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index 1f5686b1c..b788851bc 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -271,17 +271,18 @@ public class TelevisionRepository : ITelevisionRepository try { metadata.DateAdded = DateTime.UtcNow; - metadata.Genres ??= new List(); - metadata.Tags ??= new List(); - metadata.Studios ??= new List(); - metadata.Actors ??= new List(); - metadata.Guids ??= new List(); + metadata.Genres ??= []; + metadata.Tags ??= []; + metadata.Studios ??= []; + metadata.Actors ??= []; + metadata.Guids ??= []; + metadata.Artwork ??= []; var show = new Show { LibraryPathId = libraryPathId, - ShowMetadata = new List { metadata }, - Seasons = new List(), - TraktListItems = new List() + ShowMetadata = [metadata], + Seasons = [], + TraktListItems = [] }; await dbContext.Shows.AddAsync(show); @@ -670,17 +671,18 @@ public class TelevisionRepository : ITelevisionRepository LibraryPathId = libraryPathId, ShowId = show.Id, SeasonNumber = seasonNumber, - Episodes = new List(), - SeasonMetadata = new List - { - new() + Episodes = [], + SeasonMetadata = + [ + new SeasonMetadata { DateAdded = DateTime.UtcNow, - Guids = new List(), - Tags = new List() + Guids = [], + Tags = [], + Artwork = [] } - }, - TraktListItems = new List() + ], + TraktListItems = [] }; await dbContext.Seasons.AddAsync(season); await dbContext.SaveChangesAsync(); diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs index f3b80c0ee..8d5d2add0 100644 --- a/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs @@ -335,7 +335,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(), Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(), - Directors = nfo.Directors.Map(s => new Director { Name = s }).ToList() + Directors = nfo.Directors.Map(s => new Director { Name = s }).ToList(), + Artwork = [] }; } @@ -562,7 +563,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider { var updated = false; - episode.EpisodeMetadata ??= new List(); + episode.EpisodeMetadata ??= []; var toUpdate = episode.EpisodeMetadata .Where(em => episodeMetadata.Any(em2 => em2.EpisodeNumber == em.EpisodeNumber)) @@ -949,7 +950,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; metadata.ArtistId = artist.Id; - artist.ArtistMetadata = new List { metadata }; + artist.ArtistMetadata = [metadata]; return await _metadataRepository.Add(metadata); } @@ -1316,7 +1317,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider Actors = Actors(nfo.Actors, dateAdded, dateUpdated), Guids = nfo.UniqueIds .Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" }) - .ToList() + .ToList(), + Artwork = [] }; } @@ -1355,7 +1357,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider Biography = nfo.Biography, Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), Styles = nfo.Styles.Map(s => new Style { Name = s }).ToList(), - Moods = nfo.Moods.Map(m => new Mood { Name = m }).ToList() + Moods = nfo.Moods.Map(m => new Mood { Name = m }).ToList(), + Artwork = [] }; } @@ -1407,8 +1410,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(), Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(), - Studios = new List(), - Artwork = new List() + Studios = [], + Artwork = [] }; result.Add(metadata); diff --git a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs index 60d627e9f..4afe697c2 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs @@ -27,6 +27,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner private readonly IFileSystem _fileSystem; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; + private readonly IMetadataRepository _metadataRepository; private readonly ILocalSubtitlesProvider _localSubtitlesProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; @@ -67,6 +68,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner _localSubtitlesProvider = localSubtitlesProvider; _localChaptersProvider = localChaptersProvider; _localMetadataProvider = localMetadataProvider; + _metadataRepository = metadataRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; _client = client; @@ -312,11 +314,18 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner try { Movie movie = result.Item; - Option maybeArtwork = LocateArtwork(movie, artworkKind); - foreach (string posterFile in maybeArtwork) + foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone()) { - MovieMetadata metadata = movie.MovieMetadata.Head(); - await RefreshArtwork(posterFile, metadata, artworkKind, None, None, cancellationToken); + Option maybeArtwork = LocateArtwork(movie, artworkKind); + foreach (string posterFile in maybeArtwork) + { + await RefreshArtwork(posterFile, metadata, artworkKind, None, None, cancellationToken); + } + + if (maybeArtwork.IsNone && metadata.Artwork.Any(a => a.ArtworkKind == artworkKind)) + { + await _metadataRepository.RemoveArtworkWithKind(metadata, artworkKind); + } } return result; diff --git a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs index 4969679bf..a43577f21 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs @@ -23,6 +23,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan private readonly IClient _client; private readonly ILibraryRepository _libraryRepository; private readonly ILocalChaptersProvider _localChaptersProvider; + private readonly IMetadataRepository _metadataRepository; private readonly IScannerProxy _scannerProxy; private readonly IFileSystem _fileSystem; private readonly ILocalFileSystem _localFileSystem; @@ -66,6 +67,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan _localMetadataProvider = localMetadataProvider; _localSubtitlesProvider = localSubtitlesProvider; _localChaptersProvider = localChaptersProvider; + _metadataRepository = metadataRepository; _artistRepository = artistRepository; _musicVideoRepository = musicVideoRepository; _libraryRepository = libraryRepository; @@ -279,11 +281,19 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan try { Artist artist = result.Item; - await LocateArtworkForArtist(artistFolder, artworkKind).IfSomeAsync(async artworkFile => + foreach (ArtistMetadata metadata in artist.ArtistMetadata.HeadOrNone()) { - ArtistMetadata metadata = artist.ArtistMetadata.Head(); - await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); - }); + Option maybeArtwork = LocateArtworkForArtist(artistFolder, artworkKind); + foreach (string artworkFile in maybeArtwork) + { + await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); + } + + if (maybeArtwork.IsNone && metadata.Artwork.Any(a => a.ArtworkKind == artworkKind)) + { + await _metadataRepository.RemoveArtworkWithKind(metadata, artworkKind); + } + } return result; } @@ -425,7 +435,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan { if (!Optional(musicVideo.MusicVideoMetadata).Flatten().Any()) { - musicVideo.MusicVideoMetadata ??= new List(); + musicVideo.MusicVideoMetadata ??= []; string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path; _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); @@ -497,11 +507,18 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan { MusicVideo musicVideo = result.Item; - Option maybeThumbnail = LocateThumbnail(musicVideo); - foreach (string thumbnailFile in maybeThumbnail) + foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone()) { - MusicVideoMetadata metadata = musicVideo.MusicVideoMetadata.Head(); - await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, None, None, cancellationToken); + Option maybeThumbnail = LocateThumbnail(musicVideo); + foreach (string thumbnailFile in maybeThumbnail) + { + await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, None, None, cancellationToken); + } + + if (maybeThumbnail.IsNone && metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Thumbnail)) + { + await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Thumbnail); + } } return result; diff --git a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs index 79432f109..45b172fda 100644 --- a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs @@ -22,6 +22,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan private readonly IClient _client; private readonly ILibraryRepository _libraryRepository; private readonly ILocalChaptersProvider _localChaptersProvider; + private readonly IMetadataRepository _metadataRepository; private readonly IScannerProxy _scannerProxy; private readonly IFileSystem _fileSystem; private readonly ILocalFileSystem _localFileSystem; @@ -64,6 +65,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan _localMetadataProvider = localMetadataProvider; _localSubtitlesProvider = localSubtitlesProvider; _localChaptersProvider = localChaptersProvider; + _metadataRepository = metadataRepository; _otherVideoRepository = otherVideoRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; @@ -359,7 +361,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan { OtherVideo otherVideo = result.Item; - foreach (var metadata in otherVideo.OtherVideoMetadata.HeadOrNone()) + foreach (OtherVideoMetadata metadata in otherVideo.OtherVideoMetadata.HeadOrNone()) { Option maybeThumbnail = LocateThumbnail(otherVideo); foreach (string thumbnailFile in maybeThumbnail) @@ -369,7 +371,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan if (maybeThumbnail.IsNone && metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Thumbnail)) { - await _otherVideoRepository.RemoveArtwork(metadata, ArtworkKind.Thumbnail); + await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Thumbnail); } } diff --git a/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs index af34351d7..f32fecf48 100644 --- a/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs @@ -28,6 +28,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder private readonly IFileSystem _fileSystem; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; + private readonly IMetadataRepository _metadataRepository; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; private readonly IRemoteStreamRepository _remoteStreamRepository; @@ -61,6 +62,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder _fileSystem = fileSystem; _localFileSystem = localFileSystem; _localMetadataProvider = localMetadataProvider; + _metadataRepository = metadataRepository; _remoteStreamRepository = remoteStreamRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; @@ -386,11 +388,18 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder { RemoteStream remoteStream = result.Item; - Option maybeThumbnail = LocateThumbnail(remoteStream); - foreach (string thumbnailFile in maybeThumbnail) + foreach (RemoteStreamMetadata metadata in remoteStream.RemoteStreamMetadata.HeadOrNone()) { - RemoteStreamMetadata metadata = remoteStream.RemoteStreamMetadata.Head(); - await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, None, None, cancellationToken); + Option maybeThumbnail = LocateThumbnail(remoteStream); + foreach (string thumbnailFile in maybeThumbnail) + { + await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, None, None, cancellationToken); + } + + if (maybeThumbnail.IsNone && metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Thumbnail)) + { + await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Thumbnail); + } } return result; diff --git a/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs index 45b862680..49f4f86d3 100644 --- a/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs @@ -25,6 +25,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner private readonly IFileSystem _fileSystem; private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; + private readonly IMetadataRepository _metadataRepository; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; private readonly ISongRepository _songRepository; @@ -58,6 +59,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner _fileSystem = fileSystem; _localFileSystem = localFileSystem; _localMetadataProvider = localMetadataProvider; + _metadataRepository = metadataRepository; _songRepository = songRepository; _libraryRepository = libraryRepository; _mediaItemRepository = mediaItemRepository; @@ -263,7 +265,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner if (shouldUpdate) { - song.SongMetadata ??= new List(); + song.SongMetadata ??= []; _logger.LogDebug("Refreshing {Attribute} for {Path}", "Metadata", path); if (await _localMetadataProvider.RefreshTagMetadata(song)) @@ -302,23 +304,28 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner } Song song = result.Item; - Option maybeThumbnail = LocateThumbnail(song); - if (maybeThumbnail.IsNone) - { - await ExtractEmbeddedArtwork(song, ffmpegPath, cancellationToken); - } - - foreach (string thumbnailFile in maybeThumbnail) + foreach (SongMetadata metadata in song.SongMetadata.HeadOrNone()) { - SongMetadata metadata = song.SongMetadata.Head(); - await RefreshArtwork( - thumbnailFile, - metadata, - ArtworkKind.Thumbnail, - ffmpegPath, - None, - cancellationToken); + Option maybeThumbnail = LocateThumbnail(song); + if (maybeThumbnail.IsNone && !await ExtractEmbeddedArtwork(song, ffmpegPath, cancellationToken)) + { + if (metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Thumbnail)) + { + await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Thumbnail); + } + } + + foreach (string thumbnailFile in maybeThumbnail) + { + await RefreshArtwork( + thumbnailFile, + metadata, + ArtworkKind.Thumbnail, + ffmpegPath, + None, + cancellationToken); + } } return result; @@ -345,12 +352,12 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner }).Flatten(); } - private async Task ExtractEmbeddedArtwork(Song song, string ffmpegPath, CancellationToken cancellationToken) + private async Task ExtractEmbeddedArtwork(Song song, string ffmpegPath, CancellationToken cancellationToken) { Option maybeArtworkStream = Optional(song.GetHeadVersion().Streams.Find(ms => ms.AttachedPic)); foreach (MediaStream artworkStream in maybeArtworkStream) { - await RefreshArtwork( + return await RefreshArtwork( song.GetHeadVersion().MediaFiles.Head().Path, song.SongMetadata.Head(), ArtworkKind.Thumbnail, @@ -358,5 +365,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner artworkStream.Index, cancellationToken); } + + return false; } } diff --git a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs index 7bf9e73df..2ed9e5932 100644 --- a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs @@ -248,17 +248,15 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan maybeParentFolder, seasonFolder); - // skip folder if etag matches - if (knownFolder.Etag == etag) + // cache etag match for later checking + // we still need to scan the season folder in case season artwork has changed + bool etagMatch = knownFolder.Etag == etag; + if (etagMatch) { if (allTrashedItems.Any(f => f.StartsWith(seasonFolder, StringComparison.OrdinalIgnoreCase))) { _logger.LogDebug("Previously trashed items are now present in folder {Folder}", seasonFolder); - } - else - { - // etag matches and no trashed items are now present, continue to next folder - continue; + etagMatch = false; } } @@ -280,6 +278,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan foreach (Season season in maybeSeason.RightToSeq()) { + // skip scanning episodes when season folder etag matches + if (etagMatch) + { + continue; + } + Either scanResult = await ScanEpisodes( libraryPath, knownFolder, @@ -410,7 +414,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan private async Task> EnsureMetadataExists(Season season) { - season.SeasonMetadata ??= new List(); + season.SeasonMetadata ??= []; if (season.SeasonMetadata.Count == 0) { @@ -419,8 +423,9 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan SeasonId = season.Id, Season = season, DateAdded = DateTime.UtcNow, - Guids = new List(), - Tags = new List() + Guids = [], + Tags = [], + Artwork = [] }; season.SeasonMetadata.Add(metadata); @@ -492,11 +497,18 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan try { Show show = result.Item; - Option maybeArtwork = LocateArtworkForShow(showFolder, artworkKind); - foreach (string artworkFile in maybeArtwork) + foreach (ShowMetadata metadata in show.ShowMetadata.HeadOrNone()) { - ShowMetadata metadata = show.ShowMetadata.Head(); - await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); + Option maybeArtwork = LocateArtworkForShow(showFolder, artworkKind); + foreach (string artworkFile in maybeArtwork) + { + await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); + } + + if (maybeArtwork.IsNone && metadata.Artwork.Any(a => a.ArtworkKind == artworkKind)) + { + await _metadataRepository.RemoveArtworkWithKind(metadata, artworkKind); + } } return result; @@ -515,11 +527,18 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan { try { - Option maybePoster = LocatePoster(season, seasonFolder); - foreach (string posterFile in maybePoster) + foreach (SeasonMetadata metadata in season.SeasonMetadata.HeadOrNone()) { - SeasonMetadata metadata = season.SeasonMetadata.Head(); - await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None, cancellationToken); + Option maybePoster = LocatePoster(season, seasonFolder); + foreach (string posterFile in maybePoster) + { + await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None, cancellationToken); + } + + if (maybePoster.IsNone && metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Poster)) + { + await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Poster); + } } return season; @@ -535,10 +554,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan { try { - Option maybeThumbnail = LocateThumbnail(episode); - foreach (string thumbnailFile in maybeThumbnail) + foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) { - foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) + Option maybeThumbnail = LocateThumbnail(episode); + foreach (string thumbnailFile in maybeThumbnail) { await RefreshArtwork( thumbnailFile, @@ -548,6 +567,11 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan None, cancellationToken); } + + if (maybeThumbnail.IsNone && metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Thumbnail)) + { + await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Thumbnail); + } } return episode; diff --git a/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs index 7aaa8fb77..1474cac11 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs @@ -22,6 +22,7 @@ public class PlexMovieLibraryScanner : private readonly IMetadataRepository _metadataRepository; private readonly IMovieRepository _movieRepository; private readonly IPlexMovieRepository _plexMovieRepository; + private readonly IPlexMetadataRepository _plexMetadataRepository; private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexServerApiClient _plexServerApiClient; @@ -32,6 +33,7 @@ public class PlexMovieLibraryScanner : IMetadataRepository metadataRepository, IMediaSourceRepository mediaSourceRepository, IPlexMovieRepository plexMovieRepository, + IPlexMetadataRepository plexMetadataRepository, IPlexPathReplacementService plexPathReplacementService, IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, @@ -48,6 +50,7 @@ public class PlexMovieLibraryScanner : _metadataRepository = metadataRepository; _mediaSourceRepository = mediaSourceRepository; _plexMovieRepository = plexMovieRepository; + _plexMetadataRepository = plexMetadataRepository; _plexPathReplacementService = plexPathReplacementService; _logger = logger; } @@ -383,9 +386,9 @@ public class PlexMovieLibraryScanner : if (maybeIncomingArtwork.IsNone) { - existingMetadata.Artwork ??= new List(); + existingMetadata.Artwork ??= []; existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind); - await _metadataRepository.RemoveArtwork(existingMetadata, artworkKind); + await _plexMetadataRepository.RemoveArtwork(existingMetadata, artworkKind); } foreach (Artwork incomingArtwork in maybeIncomingArtwork) @@ -397,7 +400,7 @@ public class PlexMovieLibraryScanner : if (maybeExistingArtwork.IsNone) { - existingMetadata.Artwork ??= new List(); + existingMetadata.Artwork ??= []; existingMetadata.Artwork.Add(incomingArtwork); await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork); } diff --git a/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs index 36a006a8c..ffac33c50 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs @@ -22,6 +22,7 @@ public class PlexOtherVideoLibraryScanner : private readonly IMetadataRepository _metadataRepository; private readonly IOtherVideoRepository _otherVideoRepository; private readonly IPlexOtherVideoRepository _plexOtherVideoRepository; + private readonly IPlexMetadataRepository _plexMetadataRepository; private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexServerApiClient _plexServerApiClient; @@ -32,6 +33,7 @@ public class PlexOtherVideoLibraryScanner : IMetadataRepository metadataRepository, IMediaSourceRepository mediaSourceRepository, IPlexOtherVideoRepository plexOtherVideoRepository, + IPlexMetadataRepository plexMetadataRepository, IPlexPathReplacementService plexPathReplacementService, IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, @@ -48,6 +50,7 @@ public class PlexOtherVideoLibraryScanner : _metadataRepository = metadataRepository; _mediaSourceRepository = mediaSourceRepository; _plexOtherVideoRepository = plexOtherVideoRepository; + _plexMetadataRepository = plexMetadataRepository; _plexPathReplacementService = plexPathReplacementService; _logger = logger; } @@ -388,9 +391,9 @@ public class PlexOtherVideoLibraryScanner : if (maybeIncomingArtwork.IsNone) { - existingMetadata.Artwork ??= new List(); + existingMetadata.Artwork ??= []; existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind); - await _metadataRepository.RemoveArtwork(existingMetadata, artworkKind); + await _plexMetadataRepository.RemoveArtwork(existingMetadata, artworkKind); } foreach (Artwork incomingArtwork in maybeIncomingArtwork) @@ -402,7 +405,7 @@ public class PlexOtherVideoLibraryScanner : if (maybeExistingArtwork.IsNone) { - existingMetadata.Artwork ??= new List(); + existingMetadata.Artwork ??= []; existingMetadata.Artwork.Add(incomingArtwork); await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork); } diff --git a/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs index 86a625ebc..8b28e8160 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs @@ -27,6 +27,7 @@ public partial class PlexTelevisionLibraryScanner : private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexServerApiClient _plexServerApiClient; private readonly IPlexTelevisionRepository _plexTelevisionRepository; + private readonly IPlexMetadataRepository _plexMetadataRepository; private readonly ITelevisionRepository _televisionRepository; public PlexTelevisionLibraryScanner( @@ -37,6 +38,7 @@ public partial class PlexTelevisionLibraryScanner : IMediaSourceRepository mediaSourceRepository, IPlexPathReplacementService plexPathReplacementService, IPlexTelevisionRepository plexTelevisionRepository, + IPlexMetadataRepository plexMetadataRepository, IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, ILogger logger) @@ -53,6 +55,7 @@ public partial class PlexTelevisionLibraryScanner : _mediaSourceRepository = mediaSourceRepository; _plexPathReplacementService = plexPathReplacementService; _plexTelevisionRepository = plexTelevisionRepository; + _plexMetadataRepository = plexMetadataRepository; _logger = logger; } @@ -719,9 +722,9 @@ public partial class PlexTelevisionLibraryScanner : if (maybeIncomingArtwork.IsNone) { - existingMetadata.Artwork ??= new List(); + existingMetadata.Artwork ??= []; existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind); - await _metadataRepository.RemoveArtwork(existingMetadata, artworkKind); + await _plexMetadataRepository.RemoveArtwork(existingMetadata, artworkKind); } foreach (Artwork incomingArtwork in maybeIncomingArtwork) @@ -733,7 +736,7 @@ public partial class PlexTelevisionLibraryScanner : if (maybeExistingArtwork.IsNone) { - existingMetadata.Artwork ??= new List(); + existingMetadata.Artwork ??= []; existingMetadata.Artwork.Add(incomingArtwork); await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork); } diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs index f5f796284..3180da0c6 100644 --- a/ErsatzTV.Scanner/Program.cs +++ b/ErsatzTV.Scanner/Program.cs @@ -224,6 +224,7 @@ public class Program services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 219ba1fb6..3ad735a00 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -812,6 +812,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();