Browse Source

local scanner artwork cleanup (#2778)

* move plex artwork removal to its own repository

* clean up old local movie artwork

* clien up old music video/artist artwork

* clean up old remote stream artwork

* clean up old song artwork

* clean up old show artwork; properly update season artwork
pull/2779/head
Jason Dove 7 months ago committed by GitHub
parent
commit
a2d023ee69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 2
      ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs
  3. 1
      ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs
  4. 8
      ErsatzTV.Core/Interfaces/Repositories/IPlexMetadataRepository.cs
  5. 108
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  6. 11
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  7. 9
      ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs
  8. 19
      ErsatzTV.Infrastructure/Data/Repositories/PlexMetadataRepository.cs
  9. 26
      ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs
  10. 34
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  11. 17
      ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs
  12. 17
      ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs
  13. 35
      ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs
  14. 6
      ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs
  15. 17
      ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs
  16. 45
      ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs
  17. 66
      ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs
  18. 9
      ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs
  19. 9
      ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs
  20. 9
      ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs
  21. 1
      ErsatzTV.Scanner/Program.cs
  22. 1
      ErsatzTV/Startup.cs

2
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 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 - Fix playback of AC3 audio when targeting stereo output and input layout changes mid-stream
- Use other video artwork in XMLTV template - 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 ## [26.1.1] - 2026-01-08
### Fixed ### Fixed

2
ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs

@ -16,7 +16,7 @@ public interface IMetadataRepository
Task<bool> UpdateStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true); Task<bool> UpdateStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true);
Task<Unit> UpdateArtworkPath(Artwork artwork); Task<Unit> UpdateArtworkPath(Artwork artwork);
Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork); Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork);
Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind); Task<bool> RemoveArtworkWithKind(Domain.Metadata metadata, ArtworkKind artworkKind);
Task<bool> CloneArtwork( Task<bool> CloneArtwork(
Domain.Metadata metadata, Domain.Metadata metadata,

1
ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs

@ -19,5 +19,4 @@ public interface IOtherVideoRepository
Task<bool> AddActor(OtherVideoMetadata metadata, Actor actor); Task<bool> AddActor(OtherVideoMetadata metadata, Actor actor);
Task<bool> AddDirector(OtherVideoMetadata metadata, Director director); Task<bool> AddDirector(OtherVideoMetadata metadata, Director director);
Task<bool> AddWriter(OtherVideoMetadata metadata, Writer writer); Task<bool> AddWriter(OtherVideoMetadata metadata, Writer writer);
Task<bool> RemoveArtwork(OtherVideoMetadata metadata, ArtworkKind artworkKind);
} }

8
ErsatzTV.Core/Interfaces/Repositories/IPlexMetadataRepository.cs

@ -0,0 +1,8 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Repositories;
public interface IPlexMetadataRepository
{
Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind);
}

108
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -44,10 +44,11 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
{ {
MetadataKind = MetadataKind.Fallback, MetadataKind = MetadataKind.Fallback,
Title = fileName ?? showFolder, Title = fileName ?? showFolder,
Genres = new List<Genre>(), Genres = [],
Tags = new List<Tag>(), Tags = [],
Studios = new List<Studio>(), Studios = [],
Actors = new List<Actor>() Actors = [],
Artwork = []
}; };
return GetTelevisionShowMetadata(fileName, metadata); return GetTelevisionShowMetadata(fileName, metadata);
} }
@ -56,7 +57,11 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
{ {
string fileName = Path.GetFileName(artistFolder); string fileName = Path.GetFileName(artistFolder);
return new ArtistMetadata return new ArtistMetadata
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? artistFolder }; {
MetadataKind = MetadataKind.Fallback,
Title = fileName ?? artistFolder,
Artwork = []
};
} }
public List<EpisodeMetadata> GetFallbackMetadata(Episode episode) public List<EpisodeMetadata> GetFallbackMetadata(Episode episode)
@ -69,18 +74,18 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
Title = Path.GetFileNameWithoutExtension(path) ?? path, Title = Path.GetFileNameWithoutExtension(path) ?? path,
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,
EpisodeNumber = 0, EpisodeNumber = 0,
Actors = new List<Actor>(), Actors = [],
Artwork = new List<Artwork>(), Artwork = [],
Directors = new List<Director>(), Directors = [],
Genres = new List<Genre>(), Genres = [],
Guids = new List<MetadataGuid>(), Guids = [],
Studios = new List<Studio>(), Studios = [],
Tags = new List<Tag>(), Tags = [],
Writers = new List<Writer>() Writers = []
}; };
return fileName != null return fileName != null
? GetEpisodeMetadata(fileName, baseMetadata) ? GetEpisodeMetadata(fileName, baseMetadata)
: new List<EpisodeMetadata> { baseMetadata }; : [baseMetadata];
} }
public MovieMetadata GetFallbackMetadata(Movie movie) public MovieMetadata GetFallbackMetadata(Movie movie)
@ -91,13 +96,14 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
{ {
MetadataKind = MetadataKind.Fallback, MetadataKind = MetadataKind.Fallback,
Title = Path.GetFileNameWithoutExtension(path) ?? path, Title = Path.GetFileNameWithoutExtension(path) ?? path,
Genres = new List<Genre>(), Artwork = [],
Tags = new List<Tag>(), Genres = [],
Studios = new List<Studio>(), Tags = [],
Actors = new List<Actor>(), Studios = [],
Directors = new List<Director>(), Actors = [],
Writers = new List<Writer>(), Directors = [],
Guids = new List<MetadataGuid>() Writers = [],
Guids = []
}; };
return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata; return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata;
@ -110,7 +116,8 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
var metadata = new MusicVideoMetadata var metadata = new MusicVideoMetadata
{ {
MetadataKind = MetadataKind.Fallback, MetadataKind = MetadataKind.Fallback,
Title = fileName ?? path Title = fileName ?? path,
Artwork = []
}; };
return GetMusicVideoMetadata(fileName, metadata); return GetMusicVideoMetadata(fileName, metadata);
@ -125,13 +132,14 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
MetadataKind = MetadataKind.Fallback, MetadataKind = MetadataKind.Fallback,
Title = fileName ?? path, Title = fileName ?? path,
OtherVideo = otherVideo, OtherVideo = otherVideo,
Genres = new List<Genre>(), Artwork = [],
Tags = new List<Tag>(), Genres = [],
Studios = new List<Studio>(), Tags = [],
Actors = new List<Actor>(), Studios = [],
Directors = new List<Director>(), Actors = [],
Writers = new List<Writer>(), Directors = [],
Guids = new List<MetadataGuid>() Writers = [],
Guids = []
}; };
return GetOtherVideoMetadata(path, metadata); return GetOtherVideoMetadata(path, metadata);
@ -145,7 +153,8 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
{ {
MetadataKind = MetadataKind.Fallback, MetadataKind = MetadataKind.Fallback,
Title = fileName ?? path, Title = fileName ?? path,
Song = song Song = song,
Artwork = []
}; };
return GetSongMetadata(path, metadata); return GetSongMetadata(path, metadata);
@ -160,6 +169,7 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
MetadataKind = MetadataKind.Fallback, MetadataKind = MetadataKind.Fallback,
Title = fileName ?? path, Title = fileName ?? path,
Image = image, Image = image,
Artwork = [],
Genres = [], Genres = [],
Tags = [], Tags = [],
Studios = [], Studios = [],
@ -179,6 +189,7 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
MetadataKind = MetadataKind.Fallback, MetadataKind = MetadataKind.Fallback,
Title = fileName ?? path, Title = fileName ?? path,
RemoteStream = remoteStream, RemoteStream = remoteStream,
Artwork = [],
Genres = [], Genres = [],
Tags = [], Tags = [],
Studios = [], Studios = [],
@ -232,14 +243,14 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
DateAdded = baseMetadata.DateAdded, DateAdded = baseMetadata.DateAdded,
DateUpdated = baseMetadata.DateAdded, DateUpdated = baseMetadata.DateAdded,
Title = baseMetadata.Title, Title = baseMetadata.Title,
Actors = new List<Actor>(), Actors = [],
Artwork = new List<Artwork>(), Artwork = [],
Directors = new List<Director>(), Directors = [],
Genres = new List<Genre>(), Genres = [],
Guids = new List<MetadataGuid>(), Guids = [],
Studios = new List<Studio>(), Studios = [],
Tags = new List<Tag>(), Tags = [],
Writers = new List<Writer>() Writers = []
}; };
result.Add(metadata); result.Add(metadata);
@ -290,6 +301,7 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
? match.Groups[2].Value.Trim() ? match.Groups[2].Value.Trim()
: Path.GetFileNameWithoutExtension(fileName); : Path.GetFileNameWithoutExtension(fileName);
metadata.Artists = []; metadata.Artists = [];
metadata.Artwork = [];
metadata.Genres = []; metadata.Genres = [];
metadata.Tags = []; metadata.Tags = [];
metadata.Studios = []; metadata.Studios = [];
@ -326,11 +338,11 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
.Map(t => new Tag { Name = t }) .Map(t => new Tag { Name = t })
.ToList(); .ToList();
metadata.Artwork = new List<Artwork>(); metadata.Actors = [];
metadata.Actors = new List<Actor>(); metadata.Artwork = [];
metadata.Genres = new List<Genre>(); metadata.Genres = [];
metadata.Tags = tags; metadata.Tags = tags;
metadata.Studios = new List<Studio>(); metadata.Studios = [];
metadata.DateUpdated = DateTime.UtcNow; metadata.DateUpdated = DateTime.UtcNow;
metadata.OriginalTitle = Path.GetRelativePath(libraryPath, path); metadata.OriginalTitle = Path.GetRelativePath(libraryPath, path);
@ -364,8 +376,8 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
.Map(t => new Tag { Name = t }) .Map(t => new Tag { Name = t })
.ToList(); .ToList();
metadata.Artwork = [];
metadata.Actors = []; metadata.Actors = [];
metadata.Artwork = [];
metadata.Genres = []; metadata.Genres = [];
metadata.Tags = tags; metadata.Tags = tags;
metadata.Studios = []; metadata.Studios = [];
@ -402,8 +414,8 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
.Map(t => new Tag { Name = t }) .Map(t => new Tag { Name = t })
.ToList(); .ToList();
metadata.Artwork = [];
metadata.Actors = []; metadata.Actors = [];
metadata.Artwork = [];
metadata.Genres = []; metadata.Genres = [];
metadata.Tags = tags; metadata.Tags = tags;
metadata.Studios = []; metadata.Studios = [];
@ -440,11 +452,11 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
.Map(t => new Tag { Name = t }) .Map(t => new Tag { Name = t })
.ToList(); .ToList();
metadata.Artwork = new List<Artwork>(); metadata.Actors = [];
metadata.Actors = new List<Actor>(); metadata.Artwork = [];
metadata.Genres = new List<Genre>(); metadata.Genres = [];
metadata.Tags = tags; metadata.Tags = tags;
metadata.Studios = new List<Studio>(); metadata.Studios = [];
metadata.DateUpdated = DateTime.UtcNow; metadata.DateUpdated = DateTime.UtcNow;
metadata.OriginalTitle = Path.GetRelativePath(libraryPath, path); metadata.OriginalTitle = Path.GetRelativePath(libraryPath, path);

11
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -309,14 +309,13 @@ public class MetadataRepository(IDbContextFactory<TvContext> dbContextFactory) :
}; };
} }
public async Task<Unit> RemoveArtwork(Core.Domain.Metadata metadata, ArtworkKind artworkKind) public async Task<bool> 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(); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.ExecuteAsync( var ids = metadata.Artwork.Where(a => a.ArtworkKind == artworkKind).Select(a => a.Id).ToHashSet();
@"DELETE FROM Artwork WHERE ArtworkKind = @ArtworkKind AND (MovieMetadataId = @Id return await dbContext.Artwork
OR ShowMetadataId = @Id OR SeasonMetadataId = @Id OR EpisodeMetadataId = @Id OR OtherVideoMetadataId = @Id)", .Where(a => ids.Contains(a.Id))
new { ArtworkKind = artworkKind, metadata.Id }).ToUnit(); .ExecuteDeleteAsync() > 0;
} }
public async Task<bool> CloneArtwork( public async Task<bool> CloneArtwork(

9
ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs

@ -175,15 +175,6 @@ public class OtherVideoRepository : IOtherVideoRepository
new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0); new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0);
} }
public async Task<bool> 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<Either<BaseError, MediaItemScanResult<OtherVideo>>> AddOtherVideo( private async Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> AddOtherVideo(
TvContext dbContext, TvContext dbContext,
int libraryPathId, int libraryPathId,

19
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<TvContext> dbContextFactory) : IPlexMetadataRepository
{
public async Task<Unit> 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();
}
}

26
ErsatzTV.Infrastructure/Data/Repositories/RemoteStreamRepository.cs

@ -25,25 +25,27 @@ public class RemoteStreamRepository(
Option<RemoteStream> maybeExisting = await dbContext.RemoteStreams Option<RemoteStream> maybeExisting = await dbContext.RemoteStreams
.AsNoTracking() .AsNoTracking()
.Include(i => i.RemoteStreamMetadata) .Include(i => i.RemoteStreamMetadata)
.ThenInclude(ovm => ovm.Genres) .ThenInclude(rsm => rsm.Genres)
.Include(i => i.RemoteStreamMetadata) .Include(i => i.RemoteStreamMetadata)
.ThenInclude(ovm => ovm.Tags) .ThenInclude(rsm => rsm.Tags)
.Include(i => i.RemoteStreamMetadata) .Include(i => i.RemoteStreamMetadata)
.ThenInclude(ovm => ovm.Studios) .ThenInclude(rsm => rsm.Studios)
.Include(i => i.RemoteStreamMetadata) .Include(i => i.RemoteStreamMetadata)
.ThenInclude(ovm => ovm.Guids) .ThenInclude(rsm => rsm.Guids)
.Include(i => i.RemoteStreamMetadata) .Include(i => i.RemoteStreamMetadata)
.ThenInclude(ovm => ovm.Actors) .ThenInclude(rsm => rsm.Actors)
.Include(i => i.RemoteStreamMetadata) .Include(i => i.RemoteStreamMetadata)
.ThenInclude(ovm => ovm.Actors) .ThenInclude(rsm => rsm.Actors)
.ThenInclude(a => a.Artwork) .ThenInclude(a => a.Artwork)
.Include(ov => ov.LibraryPath) .Include(i => i.RemoteStreamMetadata)
.ThenInclude(rsm => rsm.Artwork)
.Include(rs => rs.LibraryPath)
.ThenInclude(lp => lp.Library) .ThenInclude(lp => lp.Library)
.Include(ov => ov.MediaVersions) .Include(rs => rs.MediaVersions)
.ThenInclude(ov => ov.MediaFiles) .ThenInclude(mv => mv.MediaFiles)
.Include(ov => ov.MediaVersions) .Include(rs => rs.MediaVersions)
.ThenInclude(ov => ov.Streams) .ThenInclude(mv => mv.Streams)
.Include(ov => ov.TraktListItems) .Include(rs => rs.TraktListItems)
.ThenInclude(tli => tli.TraktList) .ThenInclude(tli => tli.TraktList)
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path) .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path, cancellationToken); .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path, cancellationToken);

34
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -271,17 +271,18 @@ public class TelevisionRepository : ITelevisionRepository
try try
{ {
metadata.DateAdded = DateTime.UtcNow; metadata.DateAdded = DateTime.UtcNow;
metadata.Genres ??= new List<Genre>(); metadata.Genres ??= [];
metadata.Tags ??= new List<Tag>(); metadata.Tags ??= [];
metadata.Studios ??= new List<Studio>(); metadata.Studios ??= [];
metadata.Actors ??= new List<Actor>(); metadata.Actors ??= [];
metadata.Guids ??= new List<MetadataGuid>(); metadata.Guids ??= [];
metadata.Artwork ??= [];
var show = new Show var show = new Show
{ {
LibraryPathId = libraryPathId, LibraryPathId = libraryPathId,
ShowMetadata = new List<ShowMetadata> { metadata }, ShowMetadata = [metadata],
Seasons = new List<Season>(), Seasons = [],
TraktListItems = new List<TraktListItem>() TraktListItems = []
}; };
await dbContext.Shows.AddAsync(show); await dbContext.Shows.AddAsync(show);
@ -670,17 +671,18 @@ public class TelevisionRepository : ITelevisionRepository
LibraryPathId = libraryPathId, LibraryPathId = libraryPathId,
ShowId = show.Id, ShowId = show.Id,
SeasonNumber = seasonNumber, SeasonNumber = seasonNumber,
Episodes = new List<Episode>(), Episodes = [],
SeasonMetadata = new List<SeasonMetadata> SeasonMetadata =
{ [
new() new SeasonMetadata
{ {
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,
Guids = new List<MetadataGuid>(), Guids = [],
Tags = new List<Tag>() Tags = [],
Artwork = []
} }
}, ],
TraktListItems = new List<TraktListItem>() TraktListItems = []
}; };
await dbContext.Seasons.AddAsync(season); await dbContext.Seasons.AddAsync(season);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();

17
ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs

@ -335,7 +335,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(), Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
Studios = nfo.Studios.Map(s => new Studio { Name = s }).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; var updated = false;
episode.EpisodeMetadata ??= new List<EpisodeMetadata>(); episode.EpisodeMetadata ??= [];
var toUpdate = episode.EpisodeMetadata var toUpdate = episode.EpisodeMetadata
.Where(em => episodeMetadata.Any(em2 => em2.EpisodeNumber == em.EpisodeNumber)) .Where(em => episodeMetadata.Any(em2 => em2.EpisodeNumber == em.EpisodeNumber))
@ -949,7 +950,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
? SortTitle.GetSortTitle(metadata.Title) ? SortTitle.GetSortTitle(metadata.Title)
: metadata.SortTitle; : metadata.SortTitle;
metadata.ArtistId = artist.Id; metadata.ArtistId = artist.Id;
artist.ArtistMetadata = new List<ArtistMetadata> { metadata }; artist.ArtistMetadata = [metadata];
return await _metadataRepository.Add(metadata); return await _metadataRepository.Add(metadata);
} }
@ -1316,7 +1317,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider
Actors = Actors(nfo.Actors, dateAdded, dateUpdated), Actors = Actors(nfo.Actors, dateAdded, dateUpdated),
Guids = nfo.UniqueIds Guids = nfo.UniqueIds
.Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" }) .Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" })
.ToList() .ToList(),
Artwork = []
}; };
} }
@ -1355,7 +1357,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider
Biography = nfo.Biography, Biography = nfo.Biography,
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
Styles = nfo.Styles.Map(s => new Style { Name = s }).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(), Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(),
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(), Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
Studios = new List<Studio>(), Studios = [],
Artwork = new List<Artwork>() Artwork = []
}; };
result.Add(metadata); result.Add(metadata);

17
ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs

@ -27,6 +27,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly IMetadataRepository _metadataRepository;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider; private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger<MovieFolderScanner> _logger; private readonly ILogger<MovieFolderScanner> _logger;
private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaItemRepository _mediaItemRepository;
@ -67,6 +68,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
_localSubtitlesProvider = localSubtitlesProvider; _localSubtitlesProvider = localSubtitlesProvider;
_localChaptersProvider = localChaptersProvider; _localChaptersProvider = localChaptersProvider;
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_metadataRepository = metadataRepository;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
_client = client; _client = client;
@ -312,11 +314,18 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
try try
{ {
Movie movie = result.Item; Movie movie = result.Item;
Option<string> maybeArtwork = LocateArtwork(movie, artworkKind); foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone())
foreach (string posterFile in maybeArtwork)
{ {
MovieMetadata metadata = movie.MovieMetadata.Head(); Option<string> maybeArtwork = LocateArtwork(movie, artworkKind);
await RefreshArtwork(posterFile, metadata, artworkKind, None, None, cancellationToken); 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; return result;

35
ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs

@ -23,6 +23,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
private readonly IClient _client; private readonly IClient _client;
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
private readonly ILocalChaptersProvider _localChaptersProvider; private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IMetadataRepository _metadataRepository;
private readonly IScannerProxy _scannerProxy; private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
@ -66,6 +67,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_localSubtitlesProvider = localSubtitlesProvider; _localSubtitlesProvider = localSubtitlesProvider;
_localChaptersProvider = localChaptersProvider; _localChaptersProvider = localChaptersProvider;
_metadataRepository = metadataRepository;
_artistRepository = artistRepository; _artistRepository = artistRepository;
_musicVideoRepository = musicVideoRepository; _musicVideoRepository = musicVideoRepository;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
@ -279,11 +281,19 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
try try
{ {
Artist artist = result.Item; Artist artist = result.Item;
await LocateArtworkForArtist(artistFolder, artworkKind).IfSomeAsync(async artworkFile => foreach (ArtistMetadata metadata in artist.ArtistMetadata.HeadOrNone())
{ {
ArtistMetadata metadata = artist.ArtistMetadata.Head(); Option<string> maybeArtwork = LocateArtworkForArtist(artistFolder, artworkKind);
await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); 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; return result;
} }
@ -425,7 +435,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{ {
if (!Optional(musicVideo.MusicVideoMetadata).Flatten().Any()) if (!Optional(musicVideo.MusicVideoMetadata).Flatten().Any())
{ {
musicVideo.MusicVideoMetadata ??= new List<MusicVideoMetadata>(); musicVideo.MusicVideoMetadata ??= [];
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path; string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path;
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
@ -497,11 +507,18 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{ {
MusicVideo musicVideo = result.Item; MusicVideo musicVideo = result.Item;
Option<string> maybeThumbnail = LocateThumbnail(musicVideo); foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
foreach (string thumbnailFile in maybeThumbnail)
{ {
MusicVideoMetadata metadata = musicVideo.MusicVideoMetadata.Head(); Option<string> maybeThumbnail = LocateThumbnail(musicVideo);
await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, None, None, cancellationToken); 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; return result;

6
ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs

@ -22,6 +22,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
private readonly IClient _client; private readonly IClient _client;
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
private readonly ILocalChaptersProvider _localChaptersProvider; private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IMetadataRepository _metadataRepository;
private readonly IScannerProxy _scannerProxy; private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
@ -64,6 +65,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_localSubtitlesProvider = localSubtitlesProvider; _localSubtitlesProvider = localSubtitlesProvider;
_localChaptersProvider = localChaptersProvider; _localChaptersProvider = localChaptersProvider;
_metadataRepository = metadataRepository;
_otherVideoRepository = otherVideoRepository; _otherVideoRepository = otherVideoRepository;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
@ -359,7 +361,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
{ {
OtherVideo otherVideo = result.Item; OtherVideo otherVideo = result.Item;
foreach (var metadata in otherVideo.OtherVideoMetadata.HeadOrNone()) foreach (OtherVideoMetadata metadata in otherVideo.OtherVideoMetadata.HeadOrNone())
{ {
Option<string> maybeThumbnail = LocateThumbnail(otherVideo); Option<string> maybeThumbnail = LocateThumbnail(otherVideo);
foreach (string thumbnailFile in maybeThumbnail) 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)) if (maybeThumbnail.IsNone && metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Thumbnail))
{ {
await _otherVideoRepository.RemoveArtwork(metadata, ArtworkKind.Thumbnail); await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Thumbnail);
} }
} }

17
ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs

@ -28,6 +28,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly IMetadataRepository _metadataRepository;
private readonly ILogger<RemoteStreamFolderScanner> _logger; private readonly ILogger<RemoteStreamFolderScanner> _logger;
private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaItemRepository _mediaItemRepository;
private readonly IRemoteStreamRepository _remoteStreamRepository; private readonly IRemoteStreamRepository _remoteStreamRepository;
@ -61,6 +62,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
_fileSystem = fileSystem; _fileSystem = fileSystem;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_metadataRepository = metadataRepository;
_remoteStreamRepository = remoteStreamRepository; _remoteStreamRepository = remoteStreamRepository;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
@ -386,11 +388,18 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
{ {
RemoteStream remoteStream = result.Item; RemoteStream remoteStream = result.Item;
Option<string> maybeThumbnail = LocateThumbnail(remoteStream); foreach (RemoteStreamMetadata metadata in remoteStream.RemoteStreamMetadata.HeadOrNone())
foreach (string thumbnailFile in maybeThumbnail)
{ {
RemoteStreamMetadata metadata = remoteStream.RemoteStreamMetadata.Head(); Option<string> maybeThumbnail = LocateThumbnail(remoteStream);
await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail, None, None, cancellationToken); 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; return result;

45
ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs

@ -25,6 +25,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly IMetadataRepository _metadataRepository;
private readonly ILogger<SongFolderScanner> _logger; private readonly ILogger<SongFolderScanner> _logger;
private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaItemRepository _mediaItemRepository;
private readonly ISongRepository _songRepository; private readonly ISongRepository _songRepository;
@ -58,6 +59,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
_fileSystem = fileSystem; _fileSystem = fileSystem;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider; _localMetadataProvider = localMetadataProvider;
_metadataRepository = metadataRepository;
_songRepository = songRepository; _songRepository = songRepository;
_libraryRepository = libraryRepository; _libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
@ -263,7 +265,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
if (shouldUpdate) if (shouldUpdate)
{ {
song.SongMetadata ??= new List<SongMetadata>(); song.SongMetadata ??= [];
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Metadata", path); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Metadata", path);
if (await _localMetadataProvider.RefreshTagMetadata(song)) if (await _localMetadataProvider.RefreshTagMetadata(song))
@ -302,23 +304,28 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
} }
Song song = result.Item; Song song = result.Item;
Option<string> 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(); Option<string> maybeThumbnail = LocateThumbnail(song);
await RefreshArtwork( if (maybeThumbnail.IsNone && !await ExtractEmbeddedArtwork(song, ffmpegPath, cancellationToken))
thumbnailFile, {
metadata, if (metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Thumbnail))
ArtworkKind.Thumbnail, {
ffmpegPath, await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Thumbnail);
None, }
cancellationToken); }
foreach (string thumbnailFile in maybeThumbnail)
{
await RefreshArtwork(
thumbnailFile,
metadata,
ArtworkKind.Thumbnail,
ffmpegPath,
None,
cancellationToken);
}
} }
return result; return result;
@ -345,12 +352,12 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
}).Flatten(); }).Flatten();
} }
private async Task ExtractEmbeddedArtwork(Song song, string ffmpegPath, CancellationToken cancellationToken) private async Task<bool> ExtractEmbeddedArtwork(Song song, string ffmpegPath, CancellationToken cancellationToken)
{ {
Option<MediaStream> maybeArtworkStream = Optional(song.GetHeadVersion().Streams.Find(ms => ms.AttachedPic)); Option<MediaStream> maybeArtworkStream = Optional(song.GetHeadVersion().Streams.Find(ms => ms.AttachedPic));
foreach (MediaStream artworkStream in maybeArtworkStream) foreach (MediaStream artworkStream in maybeArtworkStream)
{ {
await RefreshArtwork( return await RefreshArtwork(
song.GetHeadVersion().MediaFiles.Head().Path, song.GetHeadVersion().MediaFiles.Head().Path,
song.SongMetadata.Head(), song.SongMetadata.Head(),
ArtworkKind.Thumbnail, ArtworkKind.Thumbnail,
@ -358,5 +365,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
artworkStream.Index, artworkStream.Index,
cancellationToken); cancellationToken);
} }
return false;
} }
} }

66
ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs

@ -248,17 +248,15 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
maybeParentFolder, maybeParentFolder,
seasonFolder); seasonFolder);
// skip folder if etag matches // cache etag match for later checking
if (knownFolder.Etag == etag) // 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))) if (allTrashedItems.Any(f => f.StartsWith(seasonFolder, StringComparison.OrdinalIgnoreCase)))
{ {
_logger.LogDebug("Previously trashed items are now present in folder {Folder}", seasonFolder); _logger.LogDebug("Previously trashed items are now present in folder {Folder}", seasonFolder);
} etagMatch = false;
else
{
// etag matches and no trashed items are now present, continue to next folder
continue;
} }
} }
@ -280,6 +278,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
foreach (Season season in maybeSeason.RightToSeq()) foreach (Season season in maybeSeason.RightToSeq())
{ {
// skip scanning episodes when season folder etag matches
if (etagMatch)
{
continue;
}
Either<BaseError, Unit> scanResult = await ScanEpisodes( Either<BaseError, Unit> scanResult = await ScanEpisodes(
libraryPath, libraryPath,
knownFolder, knownFolder,
@ -410,7 +414,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private async Task<Either<BaseError, Season>> EnsureMetadataExists(Season season) private async Task<Either<BaseError, Season>> EnsureMetadataExists(Season season)
{ {
season.SeasonMetadata ??= new List<SeasonMetadata>(); season.SeasonMetadata ??= [];
if (season.SeasonMetadata.Count == 0) if (season.SeasonMetadata.Count == 0)
{ {
@ -419,8 +423,9 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
SeasonId = season.Id, SeasonId = season.Id,
Season = season, Season = season,
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,
Guids = new List<MetadataGuid>(), Guids = [],
Tags = new List<Tag>() Tags = [],
Artwork = []
}; };
season.SeasonMetadata.Add(metadata); season.SeasonMetadata.Add(metadata);
@ -492,11 +497,18 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
try try
{ {
Show show = result.Item; Show show = result.Item;
Option<string> maybeArtwork = LocateArtworkForShow(showFolder, artworkKind); foreach (ShowMetadata metadata in show.ShowMetadata.HeadOrNone())
foreach (string artworkFile in maybeArtwork)
{ {
ShowMetadata metadata = show.ShowMetadata.Head(); Option<string> maybeArtwork = LocateArtworkForShow(showFolder, artworkKind);
await RefreshArtwork(artworkFile, metadata, artworkKind, None, None, cancellationToken); 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; return result;
@ -515,11 +527,18 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{ {
try try
{ {
Option<string> maybePoster = LocatePoster(season, seasonFolder); foreach (SeasonMetadata metadata in season.SeasonMetadata.HeadOrNone())
foreach (string posterFile in maybePoster)
{ {
SeasonMetadata metadata = season.SeasonMetadata.Head(); Option<string> maybePoster = LocatePoster(season, seasonFolder);
await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster, None, None, cancellationToken); 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; return season;
@ -535,10 +554,10 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{ {
try try
{ {
Option<string> maybeThumbnail = LocateThumbnail(episode); foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
foreach (string thumbnailFile in maybeThumbnail)
{ {
foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) Option<string> maybeThumbnail = LocateThumbnail(episode);
foreach (string thumbnailFile in maybeThumbnail)
{ {
await RefreshArtwork( await RefreshArtwork(
thumbnailFile, thumbnailFile,
@ -548,6 +567,11 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
None, None,
cancellationToken); cancellationToken);
} }
if (maybeThumbnail.IsNone && metadata.Artwork.Any(a => a.ArtworkKind is ArtworkKind.Thumbnail))
{
await _metadataRepository.RemoveArtworkWithKind(metadata, ArtworkKind.Thumbnail);
}
} }
return episode; return episode;

9
ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs

@ -22,6 +22,7 @@ public class PlexMovieLibraryScanner :
private readonly IMetadataRepository _metadataRepository; private readonly IMetadataRepository _metadataRepository;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
private readonly IPlexMovieRepository _plexMovieRepository; private readonly IPlexMovieRepository _plexMovieRepository;
private readonly IPlexMetadataRepository _plexMetadataRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IPlexServerApiClient _plexServerApiClient; private readonly IPlexServerApiClient _plexServerApiClient;
@ -32,6 +33,7 @@ public class PlexMovieLibraryScanner :
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexMovieRepository plexMovieRepository, IPlexMovieRepository plexMovieRepository,
IPlexMetadataRepository plexMetadataRepository,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
IFileSystem fileSystem, IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider, ILocalChaptersProvider localChaptersProvider,
@ -48,6 +50,7 @@ public class PlexMovieLibraryScanner :
_metadataRepository = metadataRepository; _metadataRepository = metadataRepository;
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_plexMovieRepository = plexMovieRepository; _plexMovieRepository = plexMovieRepository;
_plexMetadataRepository = plexMetadataRepository;
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
_logger = logger; _logger = logger;
} }
@ -383,9 +386,9 @@ public class PlexMovieLibraryScanner :
if (maybeIncomingArtwork.IsNone) if (maybeIncomingArtwork.IsNone)
{ {
existingMetadata.Artwork ??= new List<Artwork>(); existingMetadata.Artwork ??= [];
existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind); existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind);
await _metadataRepository.RemoveArtwork(existingMetadata, artworkKind); await _plexMetadataRepository.RemoveArtwork(existingMetadata, artworkKind);
} }
foreach (Artwork incomingArtwork in maybeIncomingArtwork) foreach (Artwork incomingArtwork in maybeIncomingArtwork)
@ -397,7 +400,7 @@ public class PlexMovieLibraryScanner :
if (maybeExistingArtwork.IsNone) if (maybeExistingArtwork.IsNone)
{ {
existingMetadata.Artwork ??= new List<Artwork>(); existingMetadata.Artwork ??= [];
existingMetadata.Artwork.Add(incomingArtwork); existingMetadata.Artwork.Add(incomingArtwork);
await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork); await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork);
} }

9
ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs

@ -22,6 +22,7 @@ public class PlexOtherVideoLibraryScanner :
private readonly IMetadataRepository _metadataRepository; private readonly IMetadataRepository _metadataRepository;
private readonly IOtherVideoRepository _otherVideoRepository; private readonly IOtherVideoRepository _otherVideoRepository;
private readonly IPlexOtherVideoRepository _plexOtherVideoRepository; private readonly IPlexOtherVideoRepository _plexOtherVideoRepository;
private readonly IPlexMetadataRepository _plexMetadataRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IPlexServerApiClient _plexServerApiClient; private readonly IPlexServerApiClient _plexServerApiClient;
@ -32,6 +33,7 @@ public class PlexOtherVideoLibraryScanner :
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexOtherVideoRepository plexOtherVideoRepository, IPlexOtherVideoRepository plexOtherVideoRepository,
IPlexMetadataRepository plexMetadataRepository,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
IFileSystem fileSystem, IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider, ILocalChaptersProvider localChaptersProvider,
@ -48,6 +50,7 @@ public class PlexOtherVideoLibraryScanner :
_metadataRepository = metadataRepository; _metadataRepository = metadataRepository;
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_plexOtherVideoRepository = plexOtherVideoRepository; _plexOtherVideoRepository = plexOtherVideoRepository;
_plexMetadataRepository = plexMetadataRepository;
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
_logger = logger; _logger = logger;
} }
@ -388,9 +391,9 @@ public class PlexOtherVideoLibraryScanner :
if (maybeIncomingArtwork.IsNone) if (maybeIncomingArtwork.IsNone)
{ {
existingMetadata.Artwork ??= new List<Artwork>(); existingMetadata.Artwork ??= [];
existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind); existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind);
await _metadataRepository.RemoveArtwork(existingMetadata, artworkKind); await _plexMetadataRepository.RemoveArtwork(existingMetadata, artworkKind);
} }
foreach (Artwork incomingArtwork in maybeIncomingArtwork) foreach (Artwork incomingArtwork in maybeIncomingArtwork)
@ -402,7 +405,7 @@ public class PlexOtherVideoLibraryScanner :
if (maybeExistingArtwork.IsNone) if (maybeExistingArtwork.IsNone)
{ {
existingMetadata.Artwork ??= new List<Artwork>(); existingMetadata.Artwork ??= [];
existingMetadata.Artwork.Add(incomingArtwork); existingMetadata.Artwork.Add(incomingArtwork);
await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork); await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork);
} }

9
ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs

@ -27,6 +27,7 @@ public partial class PlexTelevisionLibraryScanner :
private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IPlexServerApiClient _plexServerApiClient; private readonly IPlexServerApiClient _plexServerApiClient;
private readonly IPlexTelevisionRepository _plexTelevisionRepository; private readonly IPlexTelevisionRepository _plexTelevisionRepository;
private readonly IPlexMetadataRepository _plexMetadataRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
public PlexTelevisionLibraryScanner( public PlexTelevisionLibraryScanner(
@ -37,6 +38,7 @@ public partial class PlexTelevisionLibraryScanner :
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
IPlexTelevisionRepository plexTelevisionRepository, IPlexTelevisionRepository plexTelevisionRepository,
IPlexMetadataRepository plexMetadataRepository,
IFileSystem fileSystem, IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider, ILocalChaptersProvider localChaptersProvider,
ILogger<PlexTelevisionLibraryScanner> logger) ILogger<PlexTelevisionLibraryScanner> logger)
@ -53,6 +55,7 @@ public partial class PlexTelevisionLibraryScanner :
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
_plexTelevisionRepository = plexTelevisionRepository; _plexTelevisionRepository = plexTelevisionRepository;
_plexMetadataRepository = plexMetadataRepository;
_logger = logger; _logger = logger;
} }
@ -719,9 +722,9 @@ public partial class PlexTelevisionLibraryScanner :
if (maybeIncomingArtwork.IsNone) if (maybeIncomingArtwork.IsNone)
{ {
existingMetadata.Artwork ??= new List<Artwork>(); existingMetadata.Artwork ??= [];
existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind); existingMetadata.Artwork.RemoveAll(a => a.ArtworkKind == artworkKind);
await _metadataRepository.RemoveArtwork(existingMetadata, artworkKind); await _plexMetadataRepository.RemoveArtwork(existingMetadata, artworkKind);
} }
foreach (Artwork incomingArtwork in maybeIncomingArtwork) foreach (Artwork incomingArtwork in maybeIncomingArtwork)
@ -733,7 +736,7 @@ public partial class PlexTelevisionLibraryScanner :
if (maybeExistingArtwork.IsNone) if (maybeExistingArtwork.IsNone)
{ {
existingMetadata.Artwork ??= new List<Artwork>(); existingMetadata.Artwork ??= [];
existingMetadata.Artwork.Add(incomingArtwork); existingMetadata.Artwork.Add(incomingArtwork);
await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork); await _metadataRepository.AddArtwork(existingMetadata, incomingArtwork);
} }

1
ErsatzTV.Scanner/Program.cs

@ -224,6 +224,7 @@ public class Program
services.AddScoped<IPlexMovieRepository, PlexMovieRepository>(); services.AddScoped<IPlexMovieRepository, PlexMovieRepository>();
services.AddScoped<IPlexOtherVideoRepository, PlexOtherVideoRepository>(); services.AddScoped<IPlexOtherVideoRepository, PlexOtherVideoRepository>();
services.AddScoped<IPlexTelevisionRepository, PlexTelevisionRepository>(); services.AddScoped<IPlexTelevisionRepository, PlexTelevisionRepository>();
services.AddScoped<IPlexMetadataRepository, PlexMetadataRepository>();
services.AddScoped<IPlexPathReplacementService, PlexPathReplacementService>(); services.AddScoped<IPlexPathReplacementService, PlexPathReplacementService>();
services.AddScoped<PlexEtag>(); services.AddScoped<PlexEtag>();

1
ErsatzTV/Startup.cs

@ -812,6 +812,7 @@ public class Startup
services.AddScoped<IPlexMovieRepository, PlexMovieRepository>(); services.AddScoped<IPlexMovieRepository, PlexMovieRepository>();
services.AddScoped<IPlexTelevisionRepository, PlexTelevisionRepository>(); services.AddScoped<IPlexTelevisionRepository, PlexTelevisionRepository>();
services.AddScoped<IPlexCollectionRepository, PlexCollectionRepository>(); services.AddScoped<IPlexCollectionRepository, PlexCollectionRepository>();
services.AddScoped<IPlexMetadataRepository, PlexMetadataRepository>();
services.AddScoped<IJellyfinApiClient, JellyfinApiClient>(); services.AddScoped<IJellyfinApiClient, JellyfinApiClient>();
services.AddScoped<IJellyfinPathReplacementService, JellyfinPathReplacementService>(); services.AddScoped<IJellyfinPathReplacementService, JellyfinPathReplacementService>();
services.AddScoped<IJellyfinTelevisionRepository, JellyfinTelevisionRepository>(); services.AddScoped<IJellyfinTelevisionRepository, JellyfinTelevisionRepository>();

Loading…
Cancel
Save