sync episode tags and genres (#1155)
* sync episode tags and genres * update dependencies * property update local episode genres and tags * fix test
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -10,5 +10,6 @@ public interface IEmbyMovieLibraryScanner
|
||||
EmbyLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -10,5 +10,6 @@ public interface IEmbyTelevisionLibraryScanner
|
||||
EmbyLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -10,5 +10,6 @@ public interface IJellyfinMovieLibraryScanner
|
||||
JellyfinLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -10,5 +10,6 @@ public interface IJellyfinTelevisionLibraryScanner
|
||||
JellyfinLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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))
|
||||
|
||||
@@ -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
|
||||
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
|
||||
{
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>(),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath,
|
||||
parameters.DeepScan,
|
||||
cancellationToken),
|
||||
_ => Unit.Default
|
||||
};
|
||||
@@ -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
|
||||
bool ForceScan,
|
||||
int LibraryRefreshInterval,
|
||||
string FFmpegPath,
|
||||
string FFprobePath);
|
||||
string FFprobePath,
|
||||
bool DeepScan);
|
||||
|
||||
private record ConnectionParameters(EmbyConnection ActiveConnection)
|
||||
{
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
+6
-2
@@ -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
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath,
|
||||
parameters.DeepScan,
|
||||
cancellationToken),
|
||||
_ => Unit.Default
|
||||
};
|
||||
@@ -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
|
||||
bool ForceScan,
|
||||
int LibraryRefreshInterval,
|
||||
string FFmpegPath,
|
||||
string FFprobePath);
|
||||
string FFprobePath,
|
||||
bool DeepScan);
|
||||
|
||||
private record ConnectionParameters(JellyfinConnection ActiveConnection)
|
||||
{
|
||||
|
||||
@@ -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 :
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
false,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
false,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 :
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
false,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
false,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
.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>()
|
||||
};
|
||||
|
||||
@@ -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,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
|
||||
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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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());
|
||||
}
|
||||
});
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user