Browse Source

add deep scans for external collections (#2562)

pull/2563/head
Jason Dove 9 months ago committed by GitHub
parent
commit
e590298b93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 5
      ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs
  3. 5
      ErsatzTV.Application/Emby/Commands/SynchronizeEmbyCollections.cs
  4. 5
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinCollectionScannerHandler.cs
  5. 2
      ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinCollections.cs
  6. 5
      ErsatzTV.Application/Plex/Commands/CallPlexCollectionScannerHandler.cs
  7. 4
      ErsatzTV.Application/Plex/Commands/SynchronizePlexCollections.cs
  8. 7
      ErsatzTV.Core/Domain/MediaItem/UknownEmbyMediaItem.cs
  9. 4
      ErsatzTV.Core/Interfaces/Emby/IEmbyCollectionScanner.cs
  10. 5
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs
  11. 1
      ErsatzTV.Core/Interfaces/Plex/IPlexCollectionScanner.cs
  12. 2
      ErsatzTV.Core/Interfaces/Repositories/IEmbyCollectionRepository.cs
  13. 40
      ErsatzTV.Infrastructure/Data/Repositories/EmbyCollectionRepository.cs
  14. 4
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  15. 2
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyCollections.cs
  16. 5
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyCollectionsHandler.cs
  17. 2
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollections.cs
  18. 5
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs
  19. 2
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexCollections.cs
  20. 3
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexCollectionsHandler.cs
  21. 78
      ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs
  22. 19
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs
  23. 3
      ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs
  24. 12
      ErsatzTV.Scanner/Worker.cs
  25. 22
      ErsatzTV/Pages/Libraries.razor
  26. 6
      ErsatzTV/Services/SchedulerService.cs

4
CHANGELOG.md

@ -28,6 +28,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Poster will continue to be added as icon by default - Poster will continue to be added as icon by default
- Add buttons to edit Jellyfin and Emby connection information in **Media Sources** > **Jellyfin** and **Media Sources** > **Emby** - Add buttons to edit Jellyfin and Emby connection information in **Media Sources** > **Jellyfin** and **Media Sources** > **Emby**
- Add audio format `aac (latm)` for DVB-C compatibility; `aac` uses ADTS by default which is required in most cases - Add audio format `aac (latm)` for DVB-C compatibility; `aac` uses ADTS by default which is required in most cases
- Add deep scan option for external collections (Plex, Jellyfin, Emby)
- Jellyfin and Emby collection scans have always been deep scans
- Now, by default, they will be quick scans that trust Jellyfin and Emby's etags for detecting changes
- If a quick scan misses updating a collection, deep scans can be triggered manually
### Fixed ### Fixed
- Fix NVIDIA startup errors on arm64 - Fix NVIDIA startup errors on arm64

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

@ -91,6 +91,11 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
arguments.Add("--force"); arguments.Add("--force");
} }
if (request.DeepScan)
{
arguments.Add("--deep");
}
return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default); return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default);
} }
finally finally

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

@ -2,5 +2,6 @@ using ErsatzTV.Core;
namespace ErsatzTV.Application.Emby; namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyCollections(int EmbyMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>, public record SynchronizeEmbyCollections(int EmbyMediaSourceId, bool ForceScan, bool DeepScan)
IScannerBackgroundServiceRequest; : IRequest<Either<BaseError, Unit>>,
IScannerBackgroundServiceRequest;

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

@ -91,6 +91,11 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
arguments.Add("--force"); arguments.Add("--force");
} }
if (request.DeepScan)
{
arguments.Add("--deep");
}
return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default); return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default);
} }
finally finally

2
ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinCollections.cs

@ -2,6 +2,6 @@ using ErsatzTV.Core;
namespace ErsatzTV.Application.Jellyfin; namespace ErsatzTV.Application.Jellyfin;
public record SynchronizeJellyfinCollections(int JellyfinMediaSourceId, bool ForceScan) : public record SynchronizeJellyfinCollections(int JellyfinMediaSourceId, bool ForceScan, bool DeepScan) :
IRequest<Either<BaseError, Unit>>, IRequest<Either<BaseError, Unit>>,
IScannerBackgroundServiceRequest; IScannerBackgroundServiceRequest;

5
ErsatzTV.Application/Plex/Commands/CallPlexCollectionScannerHandler.cs

@ -91,6 +91,11 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr
arguments.Add("--force"); arguments.Add("--force");
} }
if (request.DeepScan)
{
arguments.Add("--deep");
}
return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default); return await base.PerformScan(parameters, arguments, cancellationToken).MapT(_ => Unit.Default);
} }
finally finally

4
ErsatzTV.Application/Plex/Commands/SynchronizePlexCollections.cs

@ -2,5 +2,5 @@ using ErsatzTV.Core;
namespace ErsatzTV.Application.Plex; namespace ErsatzTV.Application.Plex;
public record SynchronizePlexCollections(int PlexMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>, public record SynchronizePlexCollections(int PlexMediaSourceId, bool ForceScan, bool DeepScan)
IScannerBackgroundServiceRequest; : IRequest<Either<BaseError, Unit>>, IScannerBackgroundServiceRequest;

7
ErsatzTV.Core/Domain/MediaItem/UknownEmbyMediaItem.cs

@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public class UnknownEmbyMediaItem : MediaItem
{
public string ItemType { get; set; }
public string ItemId { get; set; }
}

4
ErsatzTV.Core/Interfaces/Emby/IEmbyCollectionScanner.cs

@ -2,7 +2,5 @@
public interface IEmbyCollectionScanner public interface IEmbyCollectionScanner
{ {
Task<Either<BaseError, Unit>> ScanCollections( Task<Either<BaseError, Unit>> ScanCollections(string address, string apiKey, bool deepScan);
string address,
string apiKey);
} }

5
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs

@ -2,8 +2,5 @@
public interface IJellyfinCollectionScanner public interface IJellyfinCollectionScanner
{ {
Task<Either<BaseError, Unit>> ScanCollections( Task<Either<BaseError, Unit>> ScanCollections(string address, string apiKey, int mediaSourceId, bool deepScan);
string address,
string apiKey,
int mediaSourceId);
} }

1
ErsatzTV.Core/Interfaces/Plex/IPlexCollectionScanner.cs

@ -8,5 +8,6 @@ public interface IPlexCollectionScanner
Task<Either<BaseError, Unit>> ScanCollections( Task<Either<BaseError, Unit>> ScanCollections(
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
bool deepScan,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

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

@ -8,6 +8,6 @@ public interface IEmbyCollectionRepository
Task<bool> AddCollection(EmbyCollection collection); Task<bool> AddCollection(EmbyCollection collection);
Task<bool> RemoveCollection(EmbyCollection collection); Task<bool> RemoveCollection(EmbyCollection collection);
Task<List<int>> RemoveAllTags(EmbyCollection collection); Task<List<int>> RemoveAllTags(EmbyCollection collection);
Task<int> AddTag(MediaItem item, EmbyCollection collection); Task<Option<int>> AddTag(MediaItem item, EmbyCollection collection);
Task<bool> SetEtag(EmbyCollection collection); Task<bool> SetEtag(EmbyCollection collection);
} }

40
ErsatzTV.Infrastructure/Data/Repositories/EmbyCollectionRepository.cs

@ -47,36 +47,36 @@ public class EmbyCollectionRepository : IEmbyCollectionRepository
// movies // movies
result.AddRange( result.AddRange(
await dbContext.Connection.QueryAsync<int>( await dbContext.Connection.QueryAsync<int>(
@"SELECT JM.Id FROM Tag T @"SELECT EM.Id FROM Tag T
INNER JOIN MovieMetadata MM on T.MovieMetadataId = MM.Id INNER JOIN MovieMetadata MM on T.MovieMetadataId = MM.Id
INNER JOIN EmbyMovie JM on JM.Id = MM.MovieId INNER JOIN EmbyMovie EM on EM.Id = MM.MovieId
WHERE T.ExternalCollectionId = @ItemId", WHERE T.ExternalCollectionId = @ItemId",
new { collection.ItemId })); new { collection.ItemId }));
// shows // shows
result.AddRange( result.AddRange(
await dbContext.Connection.QueryAsync<int>( await dbContext.Connection.QueryAsync<int>(
@"SELECT JS.Id FROM Tag T @"SELECT ES.Id FROM Tag T
INNER JOIN ShowMetadata SM on T.ShowMetadataId = SM.Id INNER JOIN ShowMetadata SM on T.ShowMetadataId = SM.Id
INNER JOIN EmbyShow JS on JS.Id = SM.ShowId INNER JOIN EmbyShow ES on ES.Id = SM.ShowId
WHERE T.ExternalCollectionId = @ItemId", WHERE T.ExternalCollectionId = @ItemId",
new { collection.ItemId })); new { collection.ItemId }));
// seasons // seasons
result.AddRange( result.AddRange(
await dbContext.Connection.QueryAsync<int>( await dbContext.Connection.QueryAsync<int>(
@"SELECT JS.Id FROM Tag T @"SELECT ES.Id FROM Tag T
INNER JOIN SeasonMetadata SM on T.SeasonMetadataId = SM.Id INNER JOIN SeasonMetadata SM on T.SeasonMetadataId = SM.Id
INNER JOIN EmbySeason JS on JS.Id = SM.SeasonId INNER JOIN EmbySeason ES on ES.Id = SM.SeasonId
WHERE T.ExternalCollectionId = @ItemId", WHERE T.ExternalCollectionId = @ItemId",
new { collection.ItemId })); new { collection.ItemId }));
// episodes // episodes
result.AddRange( result.AddRange(
await dbContext.Connection.QueryAsync<int>( await dbContext.Connection.QueryAsync<int>(
@"SELECT JE.Id FROM Tag T @"SELECT EE.Id FROM Tag T
INNER JOIN EpisodeMetadata EM on T.EpisodeMetadataId = EM.Id INNER JOIN EpisodeMetadata EM on T.EpisodeMetadataId = EM.Id
INNER JOIN EmbyEpisode JE on JE.Id = EM.EpisodeId INNER JOIN EmbyEpisode EE on EE.Id = EM.EpisodeId
WHERE T.ExternalCollectionId = @ItemId", WHERE T.ExternalCollectionId = @ItemId",
new { collection.ItemId })); new { collection.ItemId }));
@ -88,7 +88,7 @@ public class EmbyCollectionRepository : IEmbyCollectionRepository
return result; return result;
} }
public async Task<int> AddTag(MediaItem item, EmbyCollection collection) public async Task<Option<int>> AddTag(MediaItem item, EmbyCollection collection)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
switch (item) switch (item)
@ -97,6 +97,11 @@ public class EmbyCollectionRepository : IEmbyCollectionRepository
int movieId = await dbContext.Connection.ExecuteScalarAsync<int>( int movieId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT Id FROM EmbyMovie WHERE ItemId = @ItemId", @"SELECT Id FROM EmbyMovie WHERE ItemId = @ItemId",
new { movie.ItemId }); new { movie.ItemId });
if (movieId <= 0)
{
return Option<int>.None;
}
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"INSERT INTO Tag (Name, ExternalCollectionId, MovieMetadataId) @"INSERT INTO Tag (Name, ExternalCollectionId, MovieMetadataId)
SELECT @Name, @ItemId, Id FROM SELECT @Name, @ItemId, Id FROM
@ -107,6 +112,11 @@ public class EmbyCollectionRepository : IEmbyCollectionRepository
int showId = await dbContext.Connection.ExecuteScalarAsync<int>( int showId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT Id FROM EmbyShow WHERE ItemId = @ItemId", @"SELECT Id FROM EmbyShow WHERE ItemId = @ItemId",
new { show.ItemId }); new { show.ItemId });
if (showId <= 0)
{
return Option<int>.None;
}
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"INSERT INTO Tag (Name, ExternalCollectionId, ShowMetadataId) @"INSERT INTO Tag (Name, ExternalCollectionId, ShowMetadataId)
SELECT @Name, @ItemId, Id FROM SELECT @Name, @ItemId, Id FROM
@ -117,6 +127,11 @@ public class EmbyCollectionRepository : IEmbyCollectionRepository
int seasonId = await dbContext.Connection.ExecuteScalarAsync<int>( int seasonId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT Id FROM EmbySeason WHERE ItemId = @ItemId", @"SELECT Id FROM EmbySeason WHERE ItemId = @ItemId",
new { season.ItemId }); new { season.ItemId });
if (seasonId <= 0)
{
return Option<int>.None;
}
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"INSERT INTO Tag (Name, ExternalCollectionId, SeasonMetadataId) @"INSERT INTO Tag (Name, ExternalCollectionId, SeasonMetadataId)
SELECT @Name, @ItemId, Id FROM SELECT @Name, @ItemId, Id FROM
@ -127,6 +142,11 @@ public class EmbyCollectionRepository : IEmbyCollectionRepository
int episodeId = await dbContext.Connection.ExecuteScalarAsync<int>( int episodeId = await dbContext.Connection.ExecuteScalarAsync<int>(
@"SELECT Id FROM EmbyEpisode WHERE ItemId = @ItemId", @"SELECT Id FROM EmbyEpisode WHERE ItemId = @ItemId",
new { episode.ItemId }); new { episode.ItemId });
if (episodeId <= 0)
{
return Option<int>.None;
}
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"INSERT INTO Tag (Name, ExternalCollectionId, EpisodeMetadataId) @"INSERT INTO Tag (Name, ExternalCollectionId, EpisodeMetadataId)
SELECT @Name, @ItemId, Id FROM SELECT @Name, @ItemId, Id FROM
@ -134,7 +154,7 @@ public class EmbyCollectionRepository : IEmbyCollectionRepository
new { collection.Name, collection.ItemId, EpisodeId = episodeId }); new { collection.Name, collection.ItemId, EpisodeId = episodeId });
return episodeId; return episodeId;
default: default:
return 0; return Option<int>.None;
} }
} }

4
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -137,6 +137,8 @@ public class EmbyApiClient : IEmbyApiClient
if (_memoryCache.TryGetValue("emby_collections_library_item_id", out string itemId)) if (_memoryCache.TryGetValue("emby_collections_library_item_id", out string itemId))
{ {
_logger.LogDebug("Emby collections library item id is {ItemId}", itemId);
return GetPagedLibraryContents( return GetPagedLibraryContents(
address, address,
None, None,
@ -324,7 +326,7 @@ public class EmbyApiClient : IEmbyApiClient
"Series" => new EmbyShow { ItemId = item.Id }, "Series" => new EmbyShow { ItemId = item.Id },
"Season" => new EmbySeason { ItemId = item.Id }, "Season" => new EmbySeason { ItemId = item.Id },
"Episode" => new EmbyEpisode { ItemId = item.Id }, "Episode" => new EmbyEpisode { ItemId = item.Id },
_ => None _ => new UnknownEmbyMediaItem { ItemId = item.Id, ItemType = item.Type }
}; };
} }
catch (Exception ex) catch (Exception ex)

2
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyCollections.cs

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

5
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyCollectionsHandler.cs

@ -52,6 +52,7 @@ public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmby
connectionParameters, connectionParameters,
connectionParameters.MediaSource, connectionParameters.MediaSource,
request.ForceScan, request.ForceScan,
request.DeepScan,
libraryRefreshInterval, libraryRefreshInterval,
request.BaseUrl)); request.BaseUrl));
} }
@ -97,7 +98,8 @@ public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmby
{ {
Either<BaseError, Unit> result = await _scanner.ScanCollections( Either<BaseError, Unit> result = await _scanner.ScanCollections(
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey); parameters.ConnectionParameters.ApiKey,
parameters.DeepScan);
if (result.IsRight) if (result.IsRight)
{ {
@ -115,6 +117,7 @@ public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmby
ConnectionParameters ConnectionParameters, ConnectionParameters ConnectionParameters,
EmbyMediaSource MediaSource, EmbyMediaSource MediaSource,
bool ForceScan, bool ForceScan,
bool DeepScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string BaseUrl); string BaseUrl);

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

@ -2,5 +2,5 @@
namespace ErsatzTV.Scanner.Application.Jellyfin; namespace ErsatzTV.Scanner.Application.Jellyfin;
public record SynchronizeJellyfinCollections(string BaseUrl, int JellyfinMediaSourceId, bool ForceScan) public record SynchronizeJellyfinCollections(string BaseUrl, int JellyfinMediaSourceId, bool ForceScan, bool DeepScan)
: IRequest<Either<BaseError, Unit>>; : IRequest<Either<BaseError, Unit>>;

5
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs

@ -54,6 +54,7 @@ public class
connectionParameters, connectionParameters,
connectionParameters.MediaSource, connectionParameters.MediaSource,
request.ForceScan, request.ForceScan,
request.DeepScan,
libraryRefreshInterval, libraryRefreshInterval,
request.BaseUrl)); request.BaseUrl));
} }
@ -99,7 +100,8 @@ public class
Either<BaseError, Unit> result = await _scanner.ScanCollections( Either<BaseError, Unit> result = await _scanner.ScanCollections(
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey, parameters.ConnectionParameters.ApiKey,
parameters.MediaSource.Id); parameters.MediaSource.Id,
parameters.DeepScan);
if (result.IsRight) if (result.IsRight)
{ {
@ -117,6 +119,7 @@ public class
ConnectionParameters ConnectionParameters, ConnectionParameters ConnectionParameters,
JellyfinMediaSource MediaSource, JellyfinMediaSource MediaSource,
bool ForceScan, bool ForceScan,
bool DeepScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string BaseUrl); string BaseUrl);

2
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexCollections.cs

@ -2,5 +2,5 @@
namespace ErsatzTV.Scanner.Application.Plex; namespace ErsatzTV.Scanner.Application.Plex;
public record SynchronizePlexCollections(string BaseUrl, int PlexMediaSourceId, bool ForceScan) public record SynchronizePlexCollections(string BaseUrl, int PlexMediaSourceId, bool ForceScan, bool DeepScan)
: IRequest<Either<BaseError, Unit>>; : IRequest<Either<BaseError, Unit>>;

3
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexCollectionsHandler.cs

@ -52,6 +52,7 @@ public class SynchronizePlexCollectionsHandler : IRequestHandler<SynchronizePlex
connectionParameters, connectionParameters,
connectionParameters.PlexMediaSource, connectionParameters.PlexMediaSource,
request.ForceScan, request.ForceScan,
request.DeepScan,
libraryRefreshInterval, libraryRefreshInterval,
request.BaseUrl)); request.BaseUrl));
} }
@ -99,6 +100,7 @@ public class SynchronizePlexCollectionsHandler : IRequestHandler<SynchronizePlex
Either<BaseError, Unit> result = await _scanner.ScanCollections( Either<BaseError, Unit> result = await _scanner.ScanCollections(
parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken, parameters.ConnectionParameters.PlexServerAuthToken,
parameters.DeepScan,
cancellationToken); cancellationToken);
if (result.IsRight) if (result.IsRight)
@ -117,6 +119,7 @@ public class SynchronizePlexCollectionsHandler : IRequestHandler<SynchronizePlex
ConnectionParameters ConnectionParameters, ConnectionParameters ConnectionParameters,
PlexMediaSource MediaSource, PlexMediaSource MediaSource,
bool ForceScan, bool ForceScan,
bool DeepScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string BaseUrl); string BaseUrl);

78
ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs

@ -26,7 +26,7 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
_logger = logger; _logger = logger;
} }
public async Task<Either<BaseError, Unit>> ScanCollections(string address, string apiKey) public async Task<Either<BaseError, Unit>> ScanCollections(string address, string apiKey, bool deepScan)
{ {
try try
{ {
@ -46,13 +46,13 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
Option<EmbyCollection> maybeExisting = existingCollections.Find(c => c.ItemId == collection.ItemId); Option<EmbyCollection> maybeExisting = existingCollections.Find(c => c.ItemId == collection.ItemId);
// // skip if unchanged (etag) // skip if unchanged (etag)
// if (await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) == if (!deepScan && await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
// collection.Etag) collection.Etag)
// { {
// _logger.LogDebug("Emby collection {Name} is unchanged", collection.Name); _logger.LogDebug("Emby collection {Name} is unchanged", collection.Name);
// continue; continue;
// } }
// add if new // add if new
if (maybeExisting.IsNone) if (maybeExisting.IsNone)
@ -98,16 +98,74 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
List<int> removedIds = await _embyCollectionRepository.RemoveAllTags(collection); List<int> removedIds = await _embyCollectionRepository.RemoveAllTags(collection);
var movies = 0;
var shows = 0;
var seasons = 0;
var episodes = 0;
var otherTypes = new Dictionary<string, int>();
// sync tags on items // sync tags on items
var addedIds = new List<int>(); var addedIds = new List<int>();
await foreach ((MediaItem item, int _) in items) await foreach ((MediaItem item, int _) in items)
{ {
addedIds.Add(await _embyCollectionRepository.AddTag(item, collection)); Option<int> maybeId = await _embyCollectionRepository.AddTag(item, collection);
foreach (int id in maybeId)
{
addedIds.Add(id);
switch (item)
{
case Movie:
movies++;
break;
case Show:
shows++;
break;
case Season:
seasons++;
break;
case Episode:
episodes++;
break;
}
}
if (item is UnknownEmbyMediaItem unk)
{
if (!otherTypes.TryAdd(unk.ItemType, 1))
{
otherTypes[unk.ItemType]++;
}
}
}
if (addedIds.Count > 0)
{
_logger.LogDebug(
"Emby collection {Name} contains {Count} items ({Movies} movies, {Shows} shows, {Seasons} seasons, {Episodes} episodes)",
collection.Name,
addedIds.Count,
movies,
shows,
seasons,
episodes);
}
else
{
_logger.LogDebug("Emby collection {Name} contains no items that are also in ErsatzTV", collection.Name);
} }
_logger.LogDebug("Emby collection {Name} contains {Count} items", collection.Name, addedIds.Count); if (otherTypes.Count > 0)
{
_logger.LogDebug("Emby returned unsupported collection items {Items}", otherTypes);
}
int[] changedIds = removedIds.Concat(addedIds).Distinct().ToArray(); int[] changedIds = removedIds.Concat(addedIds).Distinct().ToArray();
// if (changedIds.Length > 0)
// {
// _logger.LogDebug("Reindexing ids {Ids}", changedIds.ToList());
// }
if (!await _scannerProxy.ReindexMediaItems(changedIds, CancellationToken.None)) if (!await _scannerProxy.ReindexMediaItems(changedIds, CancellationToken.None))
{ {
_logger.LogWarning("Failed to reindex media items from scanner process"); _logger.LogWarning("Failed to reindex media items from scanner process");

19
ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs

@ -26,7 +26,11 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
_logger = logger; _logger = logger;
} }
public async Task<Either<BaseError, Unit>> ScanCollections(string address, string apiKey, int mediaSourceId) public async Task<Either<BaseError, Unit>> ScanCollections(
string address,
string apiKey,
int mediaSourceId,
bool deepScan)
{ {
try try
{ {
@ -48,12 +52,13 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
Option<JellyfinCollection> maybeExisting = existingCollections.Find(c => c.ItemId == collection.ItemId); Option<JellyfinCollection> maybeExisting = existingCollections.Find(c => c.ItemId == collection.ItemId);
// // skip if unchanged (etag) // skip if unchanged (etag)
// if (await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) == collection.Etag) if (!deepScan && await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
// { collection.Etag)
// _logger.LogDebug("Jellyfin collection {Name} is unchanged", collection.Name); {
// continue; _logger.LogDebug("Jellyfin collection {Name} is unchanged", collection.Name);
// } continue;
}
// add if new // add if new
if (maybeExisting.IsNone) if (maybeExisting.IsNone)

3
ErsatzTV.Scanner/Core/Plex/PlexCollectionScanner.cs

@ -30,6 +30,7 @@ public class PlexCollectionScanner : IPlexCollectionScanner
public async Task<Either<BaseError, Unit>> ScanCollections( public async Task<Either<BaseError, Unit>> ScanCollections(
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
bool deepScan,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
try try
@ -49,7 +50,7 @@ public class PlexCollectionScanner : IPlexCollectionScanner
Option<PlexCollection> maybeExisting = existingCollections.Find(c => c.Key == collection.Key); Option<PlexCollection> maybeExisting = existingCollections.Find(c => c.Key == collection.Key);
// skip if unchanged (etag) // skip if unchanged (etag)
if (await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) == if (!deepScan && await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
collection.Etag) collection.Etag)
{ {
_logger.LogDebug("Plex collection {Name} is unchanged", collection.Name); _logger.LogDebug("Plex collection {Name} is unchanged", collection.Name);

12
ErsatzTV.Scanner/Worker.cs

@ -85,6 +85,7 @@ public class Worker : BackgroundService
scanPlexCollectionsCommand.Arguments.Add(mediaSourceIdArgument); scanPlexCollectionsCommand.Arguments.Add(mediaSourceIdArgument);
scanPlexCollectionsCommand.Arguments.Add(baseUrlArgument); scanPlexCollectionsCommand.Arguments.Add(baseUrlArgument);
scanPlexCollectionsCommand.Options.Add(forceOption); scanPlexCollectionsCommand.Options.Add(forceOption);
scanPlexCollectionsCommand.Options.Add(deepOption);
var scanPlexNetworksCommand = new Command("scan-plex-networks", "Scan Plex networks"); var scanPlexNetworksCommand = new Command("scan-plex-networks", "Scan Plex networks");
scanPlexNetworksCommand.Arguments.Add(libraryIdArgument); scanPlexNetworksCommand.Arguments.Add(libraryIdArgument);
@ -101,6 +102,7 @@ public class Worker : BackgroundService
scanEmbyCollectionsCommand.Arguments.Add(mediaSourceIdArgument); scanEmbyCollectionsCommand.Arguments.Add(mediaSourceIdArgument);
scanEmbyCollectionsCommand.Arguments.Add(baseUrlArgument); scanEmbyCollectionsCommand.Arguments.Add(baseUrlArgument);
scanEmbyCollectionsCommand.Options.Add(forceOption); scanEmbyCollectionsCommand.Options.Add(forceOption);
scanEmbyCollectionsCommand.Options.Add(deepOption);
var scanJellyfinCommand = new Command("scan-jellyfin", "Scan a Jellyfin library"); var scanJellyfinCommand = new Command("scan-jellyfin", "Scan a Jellyfin library");
scanJellyfinCommand.Arguments.Add(libraryIdArgument); scanJellyfinCommand.Arguments.Add(libraryIdArgument);
@ -112,6 +114,7 @@ public class Worker : BackgroundService
scanJellyfinCollectionsCommand.Arguments.Add(mediaSourceIdArgument); scanJellyfinCollectionsCommand.Arguments.Add(mediaSourceIdArgument);
scanJellyfinCollectionsCommand.Arguments.Add(baseUrlArgument); scanJellyfinCollectionsCommand.Arguments.Add(baseUrlArgument);
scanJellyfinCollectionsCommand.Options.Add(forceOption); scanJellyfinCollectionsCommand.Options.Add(forceOption);
scanJellyfinCollectionsCommand.Options.Add(deepOption);
// Show-specific scanning commands // Show-specific scanning commands
var showIdArgument = new Argument<int>("show-id") var showIdArgument = new Argument<int>("show-id")
@ -188,6 +191,7 @@ public class Worker : BackgroundService
{ {
if (IsScanningEnabled()) if (IsScanningEnabled())
{ {
bool deep = parseResult.GetValue(deepOption);
bool force = parseResult.GetValue(forceOption); bool force = parseResult.GetValue(forceOption);
SetProcessPriority(force); SetProcessPriority(force);
@ -201,7 +205,7 @@ public class Worker : BackgroundService
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 SynchronizePlexCollections(baseUrl, mediaSourceId, force); var scan = new SynchronizePlexCollections(baseUrl, mediaSourceId, force, deep);
await mediator.Send(scan, token); await mediator.Send(scan, token);
} }
}); });
@ -255,6 +259,7 @@ public class Worker : BackgroundService
{ {
if (IsScanningEnabled()) if (IsScanningEnabled())
{ {
bool deep = parseResult.GetValue(deepOption);
bool force = parseResult.GetValue(forceOption); bool force = parseResult.GetValue(forceOption);
SetProcessPriority(force); SetProcessPriority(force);
@ -268,7 +273,7 @@ public class Worker : BackgroundService
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 SynchronizeEmbyCollections(baseUrl, mediaSourceId, force); var scan = new SynchronizeEmbyCollections(baseUrl, mediaSourceId, force, deep);
await mediator.Send(scan, token); await mediator.Send(scan, token);
} }
}); });
@ -300,6 +305,7 @@ public class Worker : BackgroundService
{ {
if (IsScanningEnabled()) if (IsScanningEnabled())
{ {
bool deep = parseResult.GetValue(deepOption);
bool force = parseResult.GetValue(forceOption); bool force = parseResult.GetValue(forceOption);
SetProcessPriority(force); SetProcessPriority(force);
@ -313,7 +319,7 @@ public class Worker : BackgroundService
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 SynchronizeJellyfinCollections(baseUrl, mediaSourceId, force); var scan = new SynchronizeJellyfinCollections(baseUrl, mediaSourceId, force, deep);
await mediator.Send(scan, token); await mediator.Send(scan, token);
} }
}); });

22
ErsatzTV/Pages/Libraries.razor

@ -143,7 +143,19 @@
} }
else else
{ {
<div style="width: 48px"></div> if (context.LibraryKind is "Plex" or "Emby" or "Jellyfin")
{
<MudTooltip Text="Deep Scan Collections">
<MudIconButton Icon="@Icons.Material.Filled.FindReplace"
Disabled="@AreCollectionsLocked(context.LibraryKind)"
OnClick="@(_ => ScanExternalCollections(context, true))">
</MudIconButton>
</MudTooltip>
}
else
{
<div style="width: 48px"></div>
}
<MudTooltip Text="Scan Collections"> <MudTooltip Text="Scan Collections">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" <MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@AreCollectionsLocked(context.LibraryKind)" Disabled="@AreCollectionsLocked(context.LibraryKind)"
@ -235,7 +247,7 @@
} }
} }
private async Task ScanExternalCollections(LibraryViewModel library) private async Task ScanExternalCollections(LibraryViewModel library, bool deepScan = false)
{ {
var token = _cts?.Token ?? CancellationToken.None; var token = _cts?.Token ?? CancellationToken.None;
switch (library.LibraryKind.ToLowerInvariant()) switch (library.LibraryKind.ToLowerInvariant())
@ -243,21 +255,21 @@
case "emby": case "emby":
if (Locker.LockEmbyCollections()) if (Locker.LockEmbyCollections())
{ {
await ScannerWorkerChannel.WriteAsync(new SynchronizeEmbyCollections(library.MediaSourceId, true), token); await ScannerWorkerChannel.WriteAsync(new SynchronizeEmbyCollections(library.MediaSourceId, true, deepScan), token);
} }
break; break;
case "jellyfin": case "jellyfin":
if (Locker.LockJellyfinCollections()) if (Locker.LockJellyfinCollections())
{ {
await ScannerWorkerChannel.WriteAsync(new SynchronizeJellyfinCollections(library.MediaSourceId, true), token); await ScannerWorkerChannel.WriteAsync(new SynchronizeJellyfinCollections(library.MediaSourceId, true, deepScan), token);
} }
break; break;
case "plex": case "plex":
if (Locker.LockPlexCollections()) if (Locker.LockPlexCollections())
{ {
await ScannerWorkerChannel.WriteAsync(new SynchronizePlexCollections(library.MediaSourceId, true), token); await ScannerWorkerChannel.WriteAsync(new SynchronizePlexCollections(library.MediaSourceId, true, deepScan), token);
} }
break; break;

6
ErsatzTV/Services/SchedulerService.cs

@ -268,7 +268,7 @@ public class SchedulerService : BackgroundService
foreach (int mediaSourceId in mediaSourceIds) foreach (int mediaSourceId in mediaSourceIds)
{ {
await _scannerWorkerChannel.WriteAsync( await _scannerWorkerChannel.WriteAsync(
new SynchronizePlexCollections(mediaSourceId, false), new SynchronizePlexCollections(mediaSourceId, false, false),
cancellationToken); cancellationToken);
} }
} }
@ -295,7 +295,7 @@ public class SchedulerService : BackgroundService
foreach (int mediaSourceId in mediaSourceIds) foreach (int mediaSourceId in mediaSourceIds)
{ {
await _scannerWorkerChannel.WriteAsync( await _scannerWorkerChannel.WriteAsync(
new SynchronizeJellyfinCollections(mediaSourceId, false), new SynchronizeJellyfinCollections(mediaSourceId, false, false),
cancellationToken); cancellationToken);
} }
} }
@ -322,7 +322,7 @@ public class SchedulerService : BackgroundService
foreach (int mediaSourceId in mediaSourceIds) foreach (int mediaSourceId in mediaSourceIds)
{ {
await _scannerWorkerChannel.WriteAsync( await _scannerWorkerChannel.WriteAsync(
new SynchronizeEmbyCollections(mediaSourceId, false), new SynchronizeEmbyCollections(mediaSourceId, false, false),
cancellationToken); cancellationToken);
} }
} }

Loading…
Cancel
Save