using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Interfaces.Search; using Microsoft.Extensions.Logging; namespace ErsatzTV.Core.Jellyfin; public class JellyfinCollectionScanner : IJellyfinCollectionScanner { private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IJellyfinApiClient _jellyfinApiClient; private readonly IJellyfinCollectionRepository _jellyfinCollectionRepository; private readonly ILogger _logger; private readonly ISearchIndex _searchIndex; private readonly ICachingSearchRepository _searchRepository; public JellyfinCollectionScanner( IJellyfinCollectionRepository jellyfinCollectionRepository, IJellyfinApiClient jellyfinApiClient, ICachingSearchRepository searchRepository, ISearchIndex searchIndex, IFallbackMetadataProvider fallbackMetadataProvider, ILogger logger) { _jellyfinCollectionRepository = jellyfinCollectionRepository; _jellyfinApiClient = jellyfinApiClient; _searchRepository = searchRepository; _searchIndex = searchIndex; _fallbackMetadataProvider = fallbackMetadataProvider; _logger = logger; } public async Task> ScanCollections(string address, string apiKey, int mediaSourceId) { try { var incomingItemIds = new List(); // get all collections from db (item id, etag) List existingCollections = await _jellyfinCollectionRepository.GetCollections(); // loop over collections await foreach (JellyfinCollection collection in _jellyfinApiClient.GetCollectionLibraryItems( address, apiKey, mediaSourceId)) { incomingItemIds.Add(collection.ItemId); Option maybeExisting = existingCollections.Find(c => c.ItemId == collection.ItemId); // skip if unchanged (etag) if (await maybeExisting.Map(e => e.Etag ?? string.Empty).IfNoneAsync(string.Empty) == collection.Etag) { _logger.LogDebug("Jellyfin collection {Name} is unchanged", collection.Name); continue; } // add if new if (maybeExisting.IsNone) { _logger.LogDebug("Jellyfin collection {Name} is new", collection.Name); await _jellyfinCollectionRepository.AddCollection(collection); } else { _logger.LogDebug("Jellyfin collection {Name} has been updated", collection.Name); } await SyncCollectionItems(address, apiKey, mediaSourceId, collection); // save collection etag await _jellyfinCollectionRepository.SetEtag(collection); } // remove missing collections (and remove any lingering tags from those collections) foreach (JellyfinCollection collection in existingCollections.Filter( e => !incomingItemIds.Contains(e.ItemId))) { await _jellyfinCollectionRepository.RemoveCollection(collection); } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to get collections from Jellyfin"); return BaseError.New(ex.Message); } return Unit.Default; } private async Task SyncCollectionItems( string address, string apiKey, int mediaSourceId, JellyfinCollection collection) { try { // get collection items from JF IAsyncEnumerable items = _jellyfinApiClient.GetCollectionItems( address, apiKey, mediaSourceId, collection.ItemId); List removedIds = await _jellyfinCollectionRepository.RemoveAllTags(collection); // sync tags on items var addedIds = new List(); await foreach (MediaItem item in items) { addedIds.Add(await _jellyfinCollectionRepository.AddTag(item, collection)); } _logger.LogDebug("Jellyfin collection {Name} contains {Count} items", collection.Name, addedIds.Count); var changedIds = removedIds.Except(addedIds).ToList(); changedIds.AddRange(addedIds.Except(removedIds)); await _searchIndex.RebuildItems(_searchRepository, _fallbackMetadataProvider, changedIds); _searchIndex.Commit(); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to synchronize Jellyfin collection {Name}", collection.Name); } } }