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 3 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/). @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- 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
- Fix many QSV pipeline bugs

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

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

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

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

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

@ -64,6 +64,11 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync @@ -64,6 +64,11 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
{
arguments.Add("--force");
}
if (request.DeepScan)
{
arguments.Add("--deep");
}
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 @@ -7,14 +7,17 @@ public interface ISynchronizeJellyfinLibraryById : IRequest<Either<BaseError, st
{
int JellyfinLibraryId { get; }
bool ForceScan { get; }
bool DeepScan { get; }
}
public record SynchronizeJellyfinLibraryByIdIfNeeded(int JellyfinLibraryId) : ISynchronizeJellyfinLibraryById
{
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;
}

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

@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.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="Microsoft.Extensions.Caching.Memory" 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 @@ -59,6 +59,8 @@ public class FakeTelevisionRepository : ITelevisionRepository
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(EpisodeMetadata metadata, Genre genre) => 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();

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
</PropertyGroup>
<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.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" />

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

@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
</PropertyGroup>
<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="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.13.3" />

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

@ -124,7 +124,8 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -124,7 +124,8 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
public async Task<Either<BaseError, MediaItemScanResult<EmbyEpisode>>> GetOrAdd(
EmbyLibrary library,
EmbyEpisode item)
EmbyEpisode item,
bool deepScan)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<EmbyEpisode> maybeExisting = await dbContext.EmbyEpisodes
@ -158,7 +159,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -158,7 +159,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
foreach (EmbyEpisode embyEpisode in maybeExisting)
{
var result = new MediaItemScanResult<EmbyEpisode>(embyEpisode) { IsAdded = false };
if (embyEpisode.Etag != item.Etag)
if (embyEpisode.Etag != item.Etag || deepScan)
{
await UpdateEpisode(dbContext, embyEpisode, item);
result.IsUpdated = true;
@ -657,6 +658,36 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -657,6 +658,36 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
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();
foreach (Artwork artworkToRemove in metadata.Artwork
.Filter(a => !paths.Contains(a.Path))

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

@ -128,7 +128,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -128,7 +128,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
public async Task<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>> GetOrAdd(
JellyfinLibrary library,
JellyfinEpisode item)
JellyfinEpisode item,
bool deepScan)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<JellyfinEpisode> maybeExisting = await dbContext.JellyfinEpisodes
@ -162,7 +163,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -162,7 +163,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
foreach (JellyfinEpisode jellyfinEpisode in maybeExisting)
{
var result = new MediaItemScanResult<JellyfinEpisode>(jellyfinEpisode) { IsAdded = false };
if (jellyfinEpisode.Etag != item.Etag)
if (jellyfinEpisode.Etag != item.Etag || deepScan)
{
await UpdateEpisode(dbContext, jellyfinEpisode, item);
result.IsUpdated = true;
@ -660,6 +661,36 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -660,6 +661,36 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{
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();
foreach (Artwork artworkToRemove in metadata.Artwork

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

@ -183,7 +183,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -183,7 +183,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
public async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> GetOrAdd(
PlexLibrary library,
PlexEpisode item)
PlexEpisode item,
bool deepScan)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<PlexEpisode> maybeExisting = await dbContext.PlexEpisodes
@ -219,6 +220,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -219,6 +220,8 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
foreach (PlexEpisode plexEpisode in maybeExisting)
{
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)
{
await UpdateEpisodePath(dbContext, plexEpisode, item);

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

@ -539,6 +539,13 @@ public class TelevisionRepository : ITelevisionRepository @@ -539,6 +539,13 @@ public class TelevisionRepository : ITelevisionRepository
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)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();

4
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

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

2
ErsatzTV.Infrastructure/Emby/IEmbyApi.cs

@ -89,7 +89,7 @@ public interface IEmbyApi @@ -89,7 +89,7 @@ public interface IEmbyApi
string seasonId,
[Query]
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]
int startIndex = 0,
[Query]

2
ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs

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

4
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

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

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

@ -289,6 +289,58 @@ public class EpisodeNfoReaderTests @@ -289,6 +289,58 @@ public class EpisodeNfoReaderTests
.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]
public async Task FullSample_Should_Return_Nfo()

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

@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
</PropertyGroup>
<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="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" />

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -89,6 +89,12 @@ public class EpisodeNfoReader : NfoReader<EpisodeNfo>, IEpisodeNfoReader @@ -89,6 +89,12 @@ public class EpisodeNfoReader : NfoReader<EpisodeNfo>, IEpisodeNfoReader
case "plot":
await ReadStringContent(reader, nfo, (episode, plot) => episode.Plot = plot);
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":
ReadActor(reader, nfo, (episode, actor) => episode.Actors.Add(actor));
break;

8
ErsatzTV.Scanner/Worker.cs

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

4
ErsatzTV.sln

@ -31,10 +31,10 @@ Global @@ -31,10 +31,10 @@ Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{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}.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.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.Build.0 = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU

6
ErsatzTV/Pages/Libraries.razor

@ -55,7 +55,7 @@ @@ -55,7 +55,7 @@
}
else
{
if (context is PlexLibraryViewModel)
if (context is PlexLibraryViewModel or EmbyLibraryViewModel or JellyfinLibraryViewModel)
{
<MudTooltip Text="Deep Scan Library">
<MudIconButton Icon="@Icons.Material.Filled.FindReplace"
@ -120,11 +120,11 @@ @@ -120,11 +120,11 @@
break;
case JellyfinLibraryViewModel:
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;
case EmbyLibraryViewModel:
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;
}

Loading…
Cancel
Save