using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.ImportLists.Exclusions; using NzbDrone.Core.ImportLists.ImportListItems; using NzbDrone.Core.Jobs; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider.Events; using NzbDrone.Core.Tv; namespace NzbDrone.Core.ImportLists { public class ImportListSyncService : IExecute, IHandleAsync> { private readonly IImportListFactory _importListFactory; private readonly IImportListStatusService _importListStatusService; private readonly IImportListExclusionService _importListExclusionService; private readonly IImportListItemService _importListItemService; private readonly IFetchAndParseImportList _listFetcherAndParser; private readonly ISearchForNewSeries _seriesSearchService; private readonly ISeriesService _seriesService; private readonly IAddSeriesService _addSeriesService; private readonly IConfigService _configService; private readonly ITaskManager _taskManager; private readonly Logger _logger; public ImportListSyncService(IImportListFactory importListFactory, IImportListStatusService importListStatusService, IImportListExclusionService importListExclusionService, IImportListItemService importListItemService, IFetchAndParseImportList listFetcherAndParser, ISearchForNewSeries seriesSearchService, ISeriesService seriesService, IAddSeriesService addSeriesService, IConfigService configService, ITaskManager taskManager, Logger logger) { _importListFactory = importListFactory; _importListStatusService = importListStatusService; _importListExclusionService = importListExclusionService; _importListItemService = importListItemService; _listFetcherAndParser = listFetcherAndParser; _seriesSearchService = seriesSearchService; _seriesService = seriesService; _addSeriesService = addSeriesService; _configService = configService; _taskManager = taskManager; _logger = logger; } private bool AllListsSuccessfulWithAPendingClean() { var lists = _importListFactory.AutomaticAddEnabled(false); var anyRemoved = false; foreach (var list in lists) { var status = _importListStatusService.GetListStatus(list.Definition.Id); if (status.DisabledTill.HasValue) { // list failed the last time it was synced. return false; } if (!status.LastInfoSync.HasValue) { // list has never been synced. return false; } anyRemoved |= status.HasRemovedItemSinceLastClean; } return anyRemoved; } private void SyncAll() { if (_importListFactory.AutomaticAddEnabled().Empty()) { _logger.Debug("No import lists with automatic add enabled"); return; } _logger.ProgressInfo("Starting Import List Sync"); var result = _listFetcherAndParser.Fetch(); var listItems = result.Series.ToList(); ProcessListItems(listItems); TryCleanLibrary(); } private void SyncList(ImportListDefinition definition) { _logger.ProgressInfo(string.Format("Starting Import List Refresh for List {0}", definition.Name)); var result = _listFetcherAndParser.FetchSingleList(definition); var listItems = result.Series.ToList(); ProcessListItems(listItems); TryCleanLibrary(); } private void ProcessListItems(List items) { var seriesToAdd = new List(); if (items.Count == 0) { _logger.ProgressInfo("No list items to process"); return; } _logger.ProgressInfo("Processing {0} list items", items.Count); var reportNumber = 1; var listExclusions = _importListExclusionService.All(); var importLists = _importListFactory.All(); var existingTvdbIds = _seriesService.AllSeriesTvdbIds(); foreach (var item in items) { _logger.ProgressTrace("Processing list item {0}/{1}", reportNumber, items.Count); reportNumber++; var importList = importLists.Single(x => x.Id == item.ImportListId); if (!importList.EnableAutomaticAdd) { continue; } // Map by IMDb ID if we have it if (item.TvdbId <= 0 && item.ImdbId.IsNotNullOrWhiteSpace()) { var mappedSeries = _seriesSearchService.SearchForNewSeriesByImdbId(item.ImdbId) .FirstOrDefault(); if (mappedSeries != null) { item.TvdbId = mappedSeries.TvdbId; item.Title = mappedSeries?.Title; } } // Map by TMDb ID if we have it if (item.TvdbId <= 0 && item.TmdbId > 0) { var mappedSeries = _seriesSearchService.SearchForNewSeriesByTmdbId(item.TmdbId) .FirstOrDefault(); if (mappedSeries != null) { item.TvdbId = mappedSeries.TvdbId; item.Title = mappedSeries?.Title; } } // Map by AniList ID if we have it if (item.TvdbId <= 0 && item.AniListId > 0) { var mappedSeries = _seriesSearchService.SearchForNewSeriesByAniListId(item.AniListId) .FirstOrDefault(); if (mappedSeries == null) { _logger.Debug("Rejected, unable to find matching TVDB ID for Anilist ID: {0} [{1}]", item.AniListId, item.Title); continue; } item.TvdbId = mappedSeries.TvdbId; item.Title = mappedSeries.Title; } if (item.TvdbId == 0) { _logger.Debug("[{0}] Rejected, unable to find TVDB ID", item.Title); continue; } // Check to see if series excluded var excludedSeries = listExclusions.Where(s => s.TvdbId == item.TvdbId).SingleOrDefault(); if (excludedSeries != null) { _logger.Debug("{0} [{1}] Rejected due to list exclusion", item.TvdbId, item.Title); continue; } // Break if Series Exists in DB if (existingTvdbIds.Any(x => x == item.TvdbId)) { _logger.Debug("{0} [{1}] Rejected, series exists in database", item.TvdbId, item.Title); continue; } // Append Series if not already in DB or already on add list if (seriesToAdd.All(s => s.TvdbId != item.TvdbId)) { var monitored = importList.ShouldMonitor != MonitorTypes.None; seriesToAdd.Add(new Series { TvdbId = item.TvdbId, Title = item.Title, Year = item.Year, Monitored = monitored, MonitorNewItems = importList.MonitorNewItems, RootFolderPath = importList.RootFolderPath, QualityProfileId = importList.QualityProfileId, SeriesType = importList.SeriesType, SeasonFolder = importList.SeasonFolder, Seasons = item.Seasons, Tags = importList.Tags, AddOptions = new AddSeriesOptions { SearchForMissingEpisodes = importList.SearchForMissingEpisodes, // If seasons are provided use them for syncing monitored status, otherwise use the list setting. Monitor = item.Seasons.Any() ? MonitorTypes.Skip : importList.ShouldMonitor } }); } } _addSeriesService.AddSeries(seriesToAdd, true); var message = string.Format("Import List Sync Completed. Items found: {0}, Series added: {1}", items.Count, seriesToAdd.Count); _logger.ProgressInfo(message); } public void Execute(ImportListSyncCommand message) { if (message.DefinitionId.HasValue) { SyncList(_importListFactory.Get(message.DefinitionId.Value)); } else { SyncAll(); } } private void TryCleanLibrary() { if (_configService.ListSyncLevel == ListSyncLevelType.Disabled) { return; } if (AllListsSuccessfulWithAPendingClean()) { CleanLibrary(); } } private void CleanLibrary() { if (_configService.ListSyncLevel == ListSyncLevelType.Disabled) { return; } var seriesToUpdate = new List(); var seriesInLibrary = _seriesService.GetAllSeries(); foreach (var series in seriesInLibrary) { var seriesExists = _importListItemService.Exists(series.TvdbId, series.ImdbId); if (!seriesExists) { switch (_configService.ListSyncLevel) { case ListSyncLevelType.LogOnly: _logger.Info("{0} was in your library, but not found in your lists --> You might want to unmonitor or remove it", series); break; case ListSyncLevelType.KeepAndUnmonitor when series.Monitored: _logger.Info("{0} was in your library, but not found in your lists --> Keeping in library but unmonitoring it", series); series.Monitored = false; seriesToUpdate.Add(series); break; case ListSyncLevelType.KeepAndTag when !series.Tags.Contains(_configService.ListSyncTag): _logger.Info("{0} was in your library, but not found in your lists --> Keeping in library but tagging it", series); series.Tags.Add(_configService.ListSyncTag); seriesToUpdate.Add(series); break; default: break; } } } _seriesService.UpdateSeries(seriesToUpdate, true); _importListStatusService.MarkListsAsCleaned(); } public void HandleAsync(ProviderDeletedEvent message) { TryCleanLibrary(); } } }