Browse Source

sync episode tags and genres (#1155)

* sync episode tags and genres

* update dependencies

* property update local episode genres and tags

* fix test
pull/1156/head
Jason Dove 4 years ago committed by GitHub
parent
commit
13e21bbcce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 5
      ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs
  3. 7
      ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryById.cs
  4. 5
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs
  5. 5
      ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs
  6. 2
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  7. 2
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  8. 1
      ErsatzTV.Core/Interfaces/Emby/IEmbyMovieLibraryScanner.cs
  9. 1
      ErsatzTV.Core/Interfaces/Emby/IEmbyTelevisionLibraryScanner.cs
  10. 1
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs
  11. 1
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs
  12. 2
      ErsatzTV.Core/Interfaces/Repositories/IMediaServerTelevisionRepository.cs
  13. 1
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  14. 2
      ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
  15. 2
      ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj
  16. 35
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  17. 35
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  18. 5
      ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs
  19. 7
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  20. 4
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  21. 2
      ErsatzTV.Infrastructure/Emby/IEmbyApi.cs
  22. 2
      ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs
  23. 4
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  24. 52
      ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/EpisodeNfoReaderTests.cs
  25. 2
      ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
  26. 3
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryById.cs
  27. 8
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs
  28. 2
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs
  29. 8
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs
  30. 3
      ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs
  31. 3
      ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs
  32. 3
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  33. 3
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  34. 8
      ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs
  35. 2
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  36. 4
      ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfo.cs
  37. 6
      ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfoReader.cs
  38. 8
      ErsatzTV.Scanner/Worker.cs
  39. 4
      ErsatzTV.sln
  40. 6
      ErsatzTV/Pages/Libraries.razor

4
CHANGELOG.md

@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- Add button to copy/clone schedule from schedules table - Add button to copy/clone schedule from schedules table
- Synchronize episode tags and genres from Jellyfin, Emby and Local show libraries
- Add `Deep Scan` button to Jellyfin and Emby libraries
- This is now required to update some metadata for existing libraries, when targeted updates are not possible
- For example, if you already have tags and genres on your episodes in Jellyfin or Emby, you will need to deep scan each library to update that metadata on existing items in ErsatzTV
### Fixed ### Fixed
- Fix many QSV pipeline bugs - Fix many QSV pipeline bugs

5
ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs

@ -65,6 +65,11 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
arguments.Add("--force"); arguments.Add("--force");
} }
if (request.DeepScan)
{
arguments.Add("--deep");
}
return await base.PerformScan(scanner, arguments, cancellationToken); return await base.PerformScan(scanner, arguments, cancellationToken);
} }

7
ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryById.cs

@ -2,19 +2,20 @@
namespace ErsatzTV.Application.Emby; namespace ErsatzTV.Application.Emby;
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>, public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>, IEmbyBackgroundServiceRequest
IEmbyBackgroundServiceRequest
{ {
int EmbyLibraryId { get; } int EmbyLibraryId { get; }
bool ForceScan { get; } bool ForceScan { get; }
bool DeepScan { get; }
} }
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{ {
public bool ForceScan => false; public bool ForceScan => false;
public bool DeepScan => false;
} }
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId, bool DeepScan) : ISynchronizeEmbyLibraryById
{ {
public bool ForceScan => true; public bool ForceScan => true;
} }

5
ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs

@ -64,6 +64,11 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
{ {
arguments.Add("--force"); arguments.Add("--force");
} }
if (request.DeepScan)
{
arguments.Add("--deep");
}
return await base.PerformScan(scanner, arguments, cancellationToken); return await base.PerformScan(scanner, arguments, cancellationToken);
} }

5
ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs

@ -7,14 +7,17 @@ public interface ISynchronizeJellyfinLibraryById : IRequest<Either<BaseError, st
{ {
int JellyfinLibraryId { get; } int JellyfinLibraryId { get; }
bool ForceScan { get; } bool ForceScan { get; }
bool DeepScan { get; }
} }
public record SynchronizeJellyfinLibraryByIdIfNeeded(int JellyfinLibraryId) : ISynchronizeJellyfinLibraryById public record SynchronizeJellyfinLibraryByIdIfNeeded(int JellyfinLibraryId) : ISynchronizeJellyfinLibraryById
{ {
public bool ForceScan => false; public bool ForceScan => false;
public bool DeepScan => false;
} }
public record ForceSynchronizeJellyfinLibraryById(int JellyfinLibraryId) : ISynchronizeJellyfinLibraryById public record ForceSynchronizeJellyfinLibraryById
(int JellyfinLibraryId, bool DeepScan) : ISynchronizeJellyfinLibraryById
{ {
public bool ForceScan => true; public bool ForceScan => true;
} }

2
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -9,7 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" /> <PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.0" /> <PackageReference Include="CliWrap" Version="3.6.0" />
<PackageReference Include="FluentAssertions" Version="6.9.0" /> <PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.2" /> <PackageReference Include="LanguageExt.Core" Version="4.4.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />

2
ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs

@ -59,6 +59,8 @@ public class FakeTelevisionRepository : ITelevisionRepository
public Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException(); public Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException();
public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException(); public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException();
public Task<bool> AddGenre(EpisodeMetadata metadata, Genre genre) => throw new NotSupportedException();
public Task<bool> AddTag(Domain.Metadata metadata, Tag tag) => throw new NotSupportedException(); public Task<bool> AddTag(Domain.Metadata metadata, Tag tag) => throw new NotSupportedException();
public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException(); public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException();

1
ErsatzTV.Core/Interfaces/Emby/IEmbyMovieLibraryScanner.cs

@ -10,5 +10,6 @@ public interface IEmbyMovieLibraryScanner
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool deepScan,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

1
ErsatzTV.Core/Interfaces/Emby/IEmbyTelevisionLibraryScanner.cs

@ -10,5 +10,6 @@ public interface IEmbyTelevisionLibraryScanner
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool deepScan,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

1
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs

@ -10,5 +10,6 @@ public interface IJellyfinMovieLibraryScanner
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool deepScan,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

1
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs

@ -10,5 +10,6 @@ public interface IJellyfinTelevisionLibraryScanner
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool deepScan,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

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

@ -14,7 +14,7 @@ public interface IMediaServerTelevisionRepository<in TLibrary, TShow, TSeason, T
Task<List<TEtag>> GetExistingEpisodes(TLibrary library, TSeason season); Task<List<TEtag>> GetExistingEpisodes(TLibrary library, TSeason season);
Task<Either<BaseError, MediaItemScanResult<TShow>>> GetOrAdd(TLibrary library, TShow item); Task<Either<BaseError, MediaItemScanResult<TShow>>> GetOrAdd(TLibrary library, TShow item);
Task<Either<BaseError, MediaItemScanResult<TSeason>>> GetOrAdd(TLibrary library, TSeason item); Task<Either<BaseError, MediaItemScanResult<TSeason>>> GetOrAdd(TLibrary library, TSeason item);
Task<Either<BaseError, MediaItemScanResult<TEpisode>>> GetOrAdd(TLibrary library, TEpisode item); Task<Either<BaseError, MediaItemScanResult<TEpisode>>> GetOrAdd(TLibrary library, TEpisode item, bool deepScan);
Task<Unit> SetEtag(TShow show, string etag); Task<Unit> SetEtag(TShow show, string etag);
Task<Unit> SetEtag(TSeason season, string etag); Task<Unit> SetEtag(TSeason season, string etag);
Task<Unit> SetEtag(TEpisode episode, string etag); Task<Unit> SetEtag(TEpisode episode, string etag);

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

@ -30,6 +30,7 @@ public interface ITelevisionRepository
Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath); Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath);
Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath); Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath);
Task<bool> AddGenre(ShowMetadata metadata, Genre genre); Task<bool> AddGenre(ShowMetadata metadata, Genre genre);
Task<bool> AddGenre(EpisodeMetadata metadata, Genre genre);
Task<bool> AddTag(Domain.Metadata metadata, Tag tag); Task<bool> AddTag(Domain.Metadata metadata, Tag tag);
Task<bool> AddStudio(ShowMetadata metadata, Studio studio); Task<bool> AddStudio(ShowMetadata metadata, Studio studio);
Task<bool> AddActor(ShowMetadata metadata, Actor actor); Task<bool> AddActor(ShowMetadata metadata, Actor actor);

2
ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.9.0" /> <PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" /> <PackageReference Include="Moq" Version="4.18.4" />

2
ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.9.0" /> <PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" /> <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />

35
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -124,7 +124,8 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
public async Task<Either<BaseError, MediaItemScanResult<EmbyEpisode>>> GetOrAdd( public async Task<Either<BaseError, MediaItemScanResult<EmbyEpisode>>> GetOrAdd(
EmbyLibrary library, EmbyLibrary library,
EmbyEpisode item) EmbyEpisode item,
bool deepScan)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<EmbyEpisode> maybeExisting = await dbContext.EmbyEpisodes Option<EmbyEpisode> maybeExisting = await dbContext.EmbyEpisodes
@ -158,7 +159,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
foreach (EmbyEpisode embyEpisode in maybeExisting) foreach (EmbyEpisode embyEpisode in maybeExisting)
{ {
var result = new MediaItemScanResult<EmbyEpisode>(embyEpisode) { IsAdded = false }; var result = new MediaItemScanResult<EmbyEpisode>(embyEpisode) { IsAdded = false };
if (embyEpisode.Etag != item.Etag) if (embyEpisode.Etag != item.Etag || deepScan)
{ {
await UpdateEpisode(dbContext, embyEpisode, item); await UpdateEpisode(dbContext, embyEpisode, item);
result.IsUpdated = true; result.IsUpdated = true;
@ -657,6 +658,36 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
metadata.Guids.Add(guid); metadata.Guids.Add(guid);
} }
// genres
foreach (Genre genre in metadata.Genres
.Filter(g => incomingMetadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
metadata.Genres.Remove(genre);
}
foreach (Genre genre in incomingMetadata.Genres
.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
metadata.Genres.Add(genre);
}
// tags
foreach (Tag tag in metadata.Tags
.Filter(g => incomingMetadata.Tags.All(g2 => g2.Name != g.Name))
.ToList())
{
metadata.Tags.Remove(tag);
}
foreach (Tag tag in incomingMetadata.Tags
.Filter(g => metadata.Tags.All(g2 => g2.Name != g.Name))
.ToList())
{
metadata.Tags.Add(tag);
}
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList(); var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
foreach (Artwork artworkToRemove in metadata.Artwork foreach (Artwork artworkToRemove in metadata.Artwork
.Filter(a => !paths.Contains(a.Path)) .Filter(a => !paths.Contains(a.Path))

35
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -128,7 +128,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
public async Task<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>> GetOrAdd( public async Task<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>> GetOrAdd(
JellyfinLibrary library, JellyfinLibrary library,
JellyfinEpisode item) JellyfinEpisode item,
bool deepScan)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<JellyfinEpisode> maybeExisting = await dbContext.JellyfinEpisodes Option<JellyfinEpisode> maybeExisting = await dbContext.JellyfinEpisodes
@ -162,7 +163,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
foreach (JellyfinEpisode jellyfinEpisode in maybeExisting) foreach (JellyfinEpisode jellyfinEpisode in maybeExisting)
{ {
var result = new MediaItemScanResult<JellyfinEpisode>(jellyfinEpisode) { IsAdded = false }; var result = new MediaItemScanResult<JellyfinEpisode>(jellyfinEpisode) { IsAdded = false };
if (jellyfinEpisode.Etag != item.Etag) if (jellyfinEpisode.Etag != item.Etag || deepScan)
{ {
await UpdateEpisode(dbContext, jellyfinEpisode, item); await UpdateEpisode(dbContext, jellyfinEpisode, item);
result.IsUpdated = true; result.IsUpdated = true;
@ -660,6 +661,36 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{ {
metadata.Guids.Add(guid); metadata.Guids.Add(guid);
} }
// genres
foreach (Genre genre in metadata.Genres
.Filter(g => incomingMetadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
metadata.Genres.Remove(genre);
}
foreach (Genre genre in incomingMetadata.Genres
.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
metadata.Genres.Add(genre);
}
// tags
foreach (Tag tag in metadata.Tags
.Filter(g => incomingMetadata.Tags.All(g2 => g2.Name != g.Name))
.ToList())
{
metadata.Tags.Remove(tag);
}
foreach (Tag tag in incomingMetadata.Tags
.Filter(g => metadata.Tags.All(g2 => g2.Name != g.Name))
.ToList())
{
metadata.Tags.Add(tag);
}
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList(); var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
foreach (Artwork artworkToRemove in metadata.Artwork foreach (Artwork artworkToRemove in metadata.Artwork

5
ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs

@ -183,7 +183,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
public async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> GetOrAdd( public async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> GetOrAdd(
PlexLibrary library, PlexLibrary library,
PlexEpisode item) PlexEpisode item,
bool deepScan)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<PlexEpisode> maybeExisting = await dbContext.PlexEpisodes Option<PlexEpisode> maybeExisting = await dbContext.PlexEpisodes
@ -219,6 +220,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
foreach (PlexEpisode plexEpisode in maybeExisting) foreach (PlexEpisode plexEpisode in maybeExisting)
{ {
var result = new MediaItemScanResult<PlexEpisode>(plexEpisode) { IsAdded = false }; var result = new MediaItemScanResult<PlexEpisode>(plexEpisode) { IsAdded = false };
// deepScan isn't needed here since we create our own plex etags
if (plexEpisode.Etag != item.Etag) if (plexEpisode.Etag != item.Etag)
{ {
await UpdateEpisodePath(dbContext, plexEpisode, item); await UpdateEpisodePath(dbContext, plexEpisode, item);

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

@ -539,6 +539,13 @@ public class TelevisionRepository : ITelevisionRepository
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0); new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0);
} }
public async Task<bool> AddGenre(EpisodeMetadata metadata, Genre genre)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.ExecuteAsync(
"INSERT INTO Genre (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)",
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0); }
public async Task<bool> AddTag(Metadata metadata, Tag tag) public async Task<bool> AddTag(Metadata metadata, Tag tag)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();

4
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -692,8 +692,8 @@ public class EmbyApiClient : IEmbyApiClient
Plot = item.Overview, Plot = item.Overview,
Year = item.ProductionYear, Year = item.ProductionYear,
DateAdded = dateAdded, DateAdded = dateAdded,
Genres = new List<Genre>(), Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(),
Tags = new List<Tag>(), Tags = Optional(item.TagItems).Flatten().Map(t => new Tag { Name = t.Name }).ToList(),
Studios = new List<Studio>(), Studios = new List<Studio>(),
Actors = new List<Actor>(), Actors = new List<Actor>(),
Artwork = new List<Artwork>(), Artwork = new List<Artwork>(),

2
ErsatzTV.Infrastructure/Emby/IEmbyApi.cs

@ -89,7 +89,7 @@ public interface IEmbyApi
string seasonId, string seasonId,
[Query] [Query]
string fields = string fields =
"Path,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType,ProviderIds,People", "Path,Genres,Tags,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType,ProviderIds,People",
[Query] [Query]
int startIndex = 0, int startIndex = 0,
[Query] [Query]

2
ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs

@ -111,7 +111,7 @@ public interface IJellyfinApi
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
string fields = "Path,DateCreated,Etag,Overview,ProviderIds,People", string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,ProviderIds,People",
[Query] [Query]
string includeItemTypes = "Episode", string includeItemTypes = "Episode",
[Query] [Query]

4
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -775,8 +775,8 @@ public class JellyfinApiClient : IJellyfinApiClient
Plot = item.Overview, Plot = item.Overview,
Year = item.ProductionYear, Year = item.ProductionYear,
DateAdded = dateAdded, DateAdded = dateAdded,
Genres = new List<Genre>(), Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(),
Tags = new List<Tag>(), Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = new List<Studio>(), Studios = new List<Studio>(),
Actors = new List<Actor>(), Actors = new List<Actor>(),
Artwork = new List<Artwork>(), Artwork = new List<Artwork>(),

52
ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/EpisodeNfoReaderTests.cs

@ -289,6 +289,58 @@ public class EpisodeNfoReaderTests
.Should().Be(1); .Should().Be(1);
} }
} }
[Test]
public async Task Genres()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<genre>Genre 1</genre>
</episodedetails>
<episodedetails>
<genre>Genre 2</genre>
<genre>Genre 3</genre>
</episodedetails>"));
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.Count(nfo => nfo.Genres is ["Genre 1"]).Should().Be(1);
list.Count(nfo => nfo.Genres is ["Genre 2", "Genre 3"]).Should().Be(1);
}
}
[Test]
public async Task Tags()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<tag>Tag 1</tag>
</episodedetails>
<episodedetails>
<tag>Tag 2</tag>
<tag>Tag 3</tag>
</episodedetails>"));
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.Count(nfo => nfo.Tags is ["Tag 1"]).Should().Be(1);
list.Count(nfo => nfo.Tags is ["Tag 2", "Tag 3"]).Should().Be(1);
}
}
[Test] [Test]
public async Task FullSample_Should_Return_Nfo() public async Task FullSample_Should_Return_Nfo()

2
ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.9.0" /> <PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.2" /> <PackageReference Include="LanguageExt.Core" Version="4.4.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" /> <PackageReference Include="Moq" Version="4.18.4" />

3
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryById.cs

@ -2,4 +2,5 @@
namespace ErsatzTV.Scanner.Application.Emby; namespace ErsatzTV.Scanner.Application.Emby;
public record SynchronizeEmbyLibraryById(int EmbyLibraryId, bool ForceScan) : IRequest<Either<BaseError, string>>; public record SynchronizeEmbyLibraryById
(int EmbyLibraryId, bool ForceScan, bool DeepScan) : IRequest<Either<BaseError, string>>;

8
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs

@ -70,6 +70,7 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
parameters.Library, parameters.Library,
parameters.FFmpegPath, parameters.FFmpegPath,
parameters.FFprobePath, parameters.FFprobePath,
parameters.DeepScan,
cancellationToken), cancellationToken),
LibraryMediaKind.Shows => LibraryMediaKind.Shows =>
await _embyTelevisionLibraryScanner.ScanLibrary( await _embyTelevisionLibraryScanner.ScanLibrary(
@ -78,6 +79,7 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
parameters.Library, parameters.Library,
parameters.FFmpegPath, parameters.FFmpegPath,
parameters.FFprobePath, parameters.FFprobePath,
parameters.DeepScan,
cancellationToken), cancellationToken),
_ => Unit.Default _ => Unit.Default
}; };
@ -139,7 +141,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
request.ForceScan, request.ForceScan,
libraryRefreshInterval, libraryRefreshInterval,
ffmpegPath, ffmpegPath,
ffprobePath ffprobePath,
request.DeepScan
)); ));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -203,7 +206,8 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string FFmpegPath, string FFmpegPath,
string FFprobePath); string FFprobePath,
bool DeepScan);
private record ConnectionParameters(EmbyConnection ActiveConnection) private record ConnectionParameters(EmbyConnection ActiveConnection)
{ {

2
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs

@ -3,4 +3,4 @@
namespace ErsatzTV.Scanner.Application.Jellyfin; namespace ErsatzTV.Scanner.Application.Jellyfin;
public record SynchronizeJellyfinLibraryById public record SynchronizeJellyfinLibraryById
(int JellyfinLibraryId, bool ForceScan) : IRequest<Either<BaseError, string>>; (int JellyfinLibraryId, bool ForceScan, bool DeepScan) : IRequest<Either<BaseError, string>>;

8
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs

@ -82,6 +82,7 @@ public class
parameters.Library, parameters.Library,
parameters.FFmpegPath, parameters.FFmpegPath,
parameters.FFprobePath, parameters.FFprobePath,
parameters.DeepScan,
cancellationToken), cancellationToken),
LibraryMediaKind.Shows => LibraryMediaKind.Shows =>
await _jellyfinTelevisionLibraryScanner.ScanLibrary( await _jellyfinTelevisionLibraryScanner.ScanLibrary(
@ -90,6 +91,7 @@ public class
parameters.Library, parameters.Library,
parameters.FFmpegPath, parameters.FFmpegPath,
parameters.FFprobePath, parameters.FFprobePath,
parameters.DeepScan,
cancellationToken), cancellationToken),
_ => Unit.Default _ => Unit.Default
}; };
@ -151,7 +153,8 @@ public class
request.ForceScan, request.ForceScan,
libraryRefreshInterval, libraryRefreshInterval,
ffmpegPath, ffmpegPath,
ffprobePath ffprobePath,
request.DeepScan
)); ));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
@ -215,7 +218,8 @@ public class
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string FFmpegPath, string FFmpegPath,
string FFprobePath); string FFprobePath,
bool DeepScan);
private record ConnectionParameters(JellyfinConnection ActiveConnection) private record ConnectionParameters(JellyfinConnection ActiveConnection)
{ {

3
ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs

@ -50,6 +50,7 @@ public class EmbyMovieLibraryScanner :
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool deepScan,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
List<EmbyPathReplacement> pathReplacements = List<EmbyPathReplacement> pathReplacements =
@ -70,7 +71,7 @@ public class EmbyMovieLibraryScanner :
GetLocalPath, GetLocalPath,
ffmpegPath, ffmpegPath,
ffprobePath, ffprobePath,
false, deepScan,
cancellationToken); cancellationToken);
} }

3
ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -50,6 +50,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool deepScan,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
List<EmbyPathReplacement> pathReplacements = List<EmbyPathReplacement> pathReplacements =
@ -70,7 +71,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
GetLocalPath, GetLocalPath,
ffmpegPath, ffmpegPath,
ffprobePath, ffprobePath,
false, deepScan,
cancellationToken); cancellationToken);
} }

3
ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -50,6 +50,7 @@ public class JellyfinMovieLibraryScanner :
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool deepScan,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
List<JellyfinPathReplacement> pathReplacements = List<JellyfinPathReplacement> pathReplacements =
@ -70,7 +71,7 @@ public class JellyfinMovieLibraryScanner :
GetLocalPath, GetLocalPath,
ffmpegPath, ffmpegPath,
ffprobePath, ffprobePath,
false, deepScan,
cancellationToken); cancellationToken);
} }

3
ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -51,6 +51,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
bool deepScan,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
List<JellyfinPathReplacement> pathReplacements = List<JellyfinPathReplacement> pathReplacements =
@ -71,7 +72,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
GetLocalPath, GetLocalPath,
ffmpegPath, ffmpegPath,
ffprobePath, ffprobePath,
false, deepScan,
cancellationToken); cancellationToken);
} }

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

@ -443,8 +443,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider
updated = await UpdateMetadataCollections( updated = await UpdateMetadataCollections(
existing, existing,
metadata, metadata,
(_, _) => Task.FromResult(false), _televisionRepository.AddGenre,
(_, _) => Task.FromResult(false), _televisionRepository.AddTag,
(_, _) => Task.FromResult(false), (_, _) => Task.FromResult(false),
_televisionRepository.AddActor) || updated; _televisionRepository.AddActor) || updated;
@ -1120,8 +1120,8 @@ public class LocalMetadataProvider : ILocalMetadataProvider
.ToList(), .ToList(),
Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(), Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(),
Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(), Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(),
Genres = new List<Genre>(), Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
Tags = new List<Tag>(), Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
Studios = new List<Studio>(), Studios = new List<Studio>(),
Artwork = new List<Artwork>() Artwork = new List<Artwork>()
}; };

2
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -437,7 +437,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
incoming.SeasonId = season.Id; incoming.SeasonId = season.Id;
Either<BaseError, MediaItemScanResult<TEpisode>> maybeEpisode = await televisionRepository Either<BaseError, MediaItemScanResult<TEpisode>> maybeEpisode = await televisionRepository
.GetOrAdd(library, incoming) .GetOrAdd(library, incoming, deepScan)
.MapT( .MapT(
result => result =>
{ {

4
ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfo.cs

@ -4,6 +4,8 @@ public class EpisodeNfo
{ {
public EpisodeNfo() public EpisodeNfo()
{ {
Genres = new List<string>();
Tags = new List<string>();
Actors = new List<ActorNfo>(); Actors = new List<ActorNfo>();
Writers = new List<string>(); Writers = new List<string>();
Directors = new List<string>(); Directors = new List<string>();
@ -17,6 +19,8 @@ public class EpisodeNfo
public string? ContentRating { get; set; } public string? ContentRating { get; set; }
public Option<DateTime> Aired { get; set; } public Option<DateTime> Aired { get; set; }
public string? Plot { get; set; } public string? Plot { get; set; }
public List<string> Genres { get; }
public List<string> Tags { get; }
public List<ActorNfo> Actors { get; } public List<ActorNfo> Actors { get; }
public List<string> Writers { get; } public List<string> Writers { get; }
public List<string> Directors { get; } public List<string> Directors { get; }

6
ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfoReader.cs

@ -89,6 +89,12 @@ public class EpisodeNfoReader : NfoReader<EpisodeNfo>, IEpisodeNfoReader
case "plot": case "plot":
await ReadStringContent(reader, nfo, (episode, plot) => episode.Plot = plot); await ReadStringContent(reader, nfo, (episode, plot) => episode.Plot = plot);
break; break;
case "genre":
await ReadStringContent(reader, nfo, (episode, genre) => episode.Genres.Add(genre));
break;
case "tag":
await ReadStringContent(reader, nfo, (episode, tag) => episode.Tags.Add(tag));
break;
case "actor": case "actor":
ReadActor(reader, nfo, (episode, actor) => episode.Actors.Add(actor)); ReadActor(reader, nfo, (episode, actor) => episode.Actors.Add(actor));
break; break;

8
ErsatzTV.Scanner/Worker.cs

@ -72,10 +72,12 @@ public class Worker : BackgroundService
var scanEmbyCommand = new Command("scan-emby", "Scan an Emby library"); var scanEmbyCommand = new Command("scan-emby", "Scan an Emby library");
scanEmbyCommand.AddArgument(libraryIdArgument); scanEmbyCommand.AddArgument(libraryIdArgument);
scanEmbyCommand.AddOption(forceOption); scanEmbyCommand.AddOption(forceOption);
scanEmbyCommand.AddOption(deepOption);
var scanJellyfinCommand = new Command("scan-jellyfin", "Scan a Jellyfin library"); var scanJellyfinCommand = new Command("scan-jellyfin", "Scan a Jellyfin library");
scanJellyfinCommand.AddArgument(libraryIdArgument); scanJellyfinCommand.AddArgument(libraryIdArgument);
scanJellyfinCommand.AddOption(forceOption); scanJellyfinCommand.AddOption(forceOption);
scanJellyfinCommand.AddOption(deepOption);
scanLocalCommand.SetHandler( scanLocalCommand.SetHandler(
async context => async context =>
@ -122,12 +124,13 @@ public class Worker : BackgroundService
bool force = context.ParseResult.GetValueForOption(forceOption); bool force = context.ParseResult.GetValueForOption(forceOption);
SetProcessPriority(force); SetProcessPriority(force);
bool deep = context.ParseResult.GetValueForOption(deepOption);
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument); int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>(); IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var scan = new SynchronizeEmbyLibraryById(libraryId, force); var scan = new SynchronizeEmbyLibraryById(libraryId, force, deep);
await mediator.Send(scan, context.GetCancellationToken()); await mediator.Send(scan, context.GetCancellationToken());
} }
}); });
@ -140,12 +143,13 @@ public class Worker : BackgroundService
bool force = context.ParseResult.GetValueForOption(forceOption); bool force = context.ParseResult.GetValueForOption(forceOption);
SetProcessPriority(force); SetProcessPriority(force);
bool deep = context.ParseResult.GetValueForOption(deepOption);
int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument); int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument);
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>(); IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var scan = new SynchronizeJellyfinLibraryById(libraryId, force); var scan = new SynchronizeJellyfinLibraryById(libraryId, force, deep);
await mediator.Send(scan, context.GetCancellationToken()); await mediator.Send(scan, context.GetCancellationToken());
} }
}); });

4
ErsatzTV.sln

@ -31,10 +31,10 @@ Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.Build.0 = Release|Any CPU {E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.Build.0 = Release|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.ActiveCfg = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.Build.0 = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU {C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU

6
ErsatzTV/Pages/Libraries.razor

@ -55,7 +55,7 @@
} }
else else
{ {
if (context is PlexLibraryViewModel) if (context is PlexLibraryViewModel or EmbyLibraryViewModel or JellyfinLibraryViewModel)
{ {
<MudTooltip Text="Deep Scan Library"> <MudTooltip Text="Deep Scan Library">
<MudIconButton Icon="@Icons.Material.Filled.FindReplace" <MudIconButton Icon="@Icons.Material.Filled.FindReplace"
@ -120,11 +120,11 @@
break; break;
case JellyfinLibraryViewModel: case JellyfinLibraryViewModel:
await _jellyfinWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(library.MediaSourceId), _cts.Token); await _jellyfinWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(library.MediaSourceId), _cts.Token);
await _jellyfinWorkerChannel.WriteAsync(new ForceSynchronizeJellyfinLibraryById(library.Id), _cts.Token); await _jellyfinWorkerChannel.WriteAsync(new ForceSynchronizeJellyfinLibraryById(library.Id, deepScan), _cts.Token);
break; break;
case EmbyLibraryViewModel: case EmbyLibraryViewModel:
await _embyWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(library.MediaSourceId), _cts.Token); await _embyWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(library.MediaSourceId), _cts.Token);
await _embyWorkerChannel.WriteAsync(new ForceSynchronizeEmbyLibraryById(library.Id), _cts.Token); await _embyWorkerChannel.WriteAsync(new ForceSynchronizeEmbyLibraryById(library.Id, deepScan), _cts.Token);
break; break;
} }

Loading…
Cancel
Save