From 1c6a32b6848140da4df20c803c0642e32481dd63 Mon Sep 17 00:00:00 2001 From: geogolem Date: Sat, 25 Feb 2017 16:38:52 -0500 Subject: [PATCH] List sync with removal (#656) --- .../Config/NetImportConfigResource.cs | 6 +- src/NzbDrone.Api/Series/MovieLookupModule.cs | 7 ++ src/NzbDrone.Api/Series/MovieModule.cs | 8 +- .../Configuration/ConfigService.cs | 12 +++ .../Configuration/IConfigService.cs | 2 + .../MediaFiles/MediaFileService.cs | 2 +- .../NetImport/NetImportSearchService.cs | 83 ++++++++++++++++++- src/NzbDrone.Core/Tv/MovieService.cs | 45 +++++++++- src/UI/Movies/Delete/DeleteMovieTemplate.hbs | 24 ++++++ src/UI/Movies/Delete/DeleteMovieView.js | 9 +- .../NetImport/Options/NetImportOptionsView.js | 63 +++++++++++++- .../Options/NetImportOptionsViewTemplate.hbs | 30 ++++++- 12 files changed, 276 insertions(+), 15 deletions(-) diff --git a/src/NzbDrone.Api/Config/NetImportConfigResource.cs b/src/NzbDrone.Api/Config/NetImportConfigResource.cs index a1502375c..6dae86e4d 100644 --- a/src/NzbDrone.Api/Config/NetImportConfigResource.cs +++ b/src/NzbDrone.Api/Config/NetImportConfigResource.cs @@ -6,6 +6,8 @@ namespace NzbDrone.Api.Config public class NetImportConfigResource : RestResource { public int NetImportSyncInterval { get; set; } + public string ListSyncLevel { get; set; } + public string ImportExclusions { get; set; } } public static class NetImportConfigResourceMapper @@ -14,7 +16,9 @@ namespace NzbDrone.Api.Config { return new NetImportConfigResource { - NetImportSyncInterval = model.NetImportSyncInterval + NetImportSyncInterval = model.NetImportSyncInterval, + ListSyncLevel = model.ListSyncLevel, + ImportExclusions = model.ImportExclusions, }; } } diff --git a/src/NzbDrone.Api/Series/MovieLookupModule.cs b/src/NzbDrone.Api/Series/MovieLookupModule.cs index fcf0ca5a4..e3ced5cb8 100644 --- a/src/NzbDrone.Api/Series/MovieLookupModule.cs +++ b/src/NzbDrone.Api/Series/MovieLookupModule.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Api.Movie _searchProxy = searchProxy; Get["/"] = x => Search(); Get["/tmdb"] = x => SearchByTmdbId(); + Get["/imdb"] = x => SearchByImdbId(); } private Response SearchByTmdbId() @@ -35,6 +36,12 @@ namespace NzbDrone.Api.Movie throw new BadRequestException("Tmdb Id was not valid"); } + private Response SearchByImdbId() + { + string imdbId = Request.Query.imdbId; + var result = _movieInfo.GetMovieInfo(imdbId); + return result.ToResource().AsResponse(); + } private Response Search() { diff --git a/src/NzbDrone.Api/Series/MovieModule.cs b/src/NzbDrone.Api/Series/MovieModule.cs index 2c9728422..b08d90747 100644 --- a/src/NzbDrone.Api/Series/MovieModule.cs +++ b/src/NzbDrone.Api/Series/MovieModule.cs @@ -186,14 +186,20 @@ namespace NzbDrone.Api.Movie private void DeleteMovie(int id) { var deleteFiles = false; + var addExclusion = false; var deleteFilesQuery = Request.Query.deleteFiles; + var addExclusionQuery = Request.Query.addExclusion; if (deleteFilesQuery.HasValue) { deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value); } + if (addExclusionQuery.HasValue) + { + addExclusion = Convert.ToBoolean(addExclusionQuery.Value); + } - _moviesService.DeleteMovie(id, deleteFiles); + _moviesService.DeleteMovie(id, deleteFiles, addExclusion); } private void MapCoversToLocal(params MovieResource[] movies) diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index e4c14918a..c37d71a6c 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -118,6 +118,18 @@ namespace NzbDrone.Core.Configuration set { SetValue("NetImportSyncInterval", value); } } + public string ListSyncLevel + { + get { return GetValue("ListSyncLevel", "disabled"); } + set { SetValue("ListSyncLevel", value); } + } + + public string ImportExclusions + { + get { return GetValue("ImportExclusions", string.Empty); } + set { SetValue("ImportExclusions", value); } + } + public int MinimumAge { get { return GetValueInt("MinimumAge", 0); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 73d45d37e..7a4ca2625 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -49,6 +49,8 @@ namespace NzbDrone.Core.Configuration int AvailabilityDelay { get; set; } int NetImportSyncInterval { get; set; } + string ListSyncLevel { get; set; } + string ImportExclusions { get; set; } //UI int FirstDayOfWeek { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index 97f1bfaea..2bf40995f 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -172,4 +172,4 @@ namespace NzbDrone.Core.MediaFiles } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs index 6e9cedda3..a93b9fe92 100644 --- a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs +++ b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs @@ -1,10 +1,13 @@ using System.Collections.Generic; +using System; using System.Linq; using NLog; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; +using NzbDrone.Core.Configuration; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.NetImport { @@ -21,15 +24,18 @@ namespace NzbDrone.Core.NetImport private readonly IMovieService _movieService; private readonly ISearchForNewMovie _movieSearch; private readonly IRootFolderService _rootFolder; + private readonly IConfigService _configService; + public NetImportSearchService(INetImportFactory netImportFactory, IMovieService movieService, - ISearchForNewMovie movieSearch, IRootFolderService rootFolder, Logger logger) + ISearchForNewMovie movieSearch, IRootFolderService rootFolder, IConfigService configService, Logger logger) { _netImportFactory = netImportFactory; _movieService = movieService; _movieSearch = movieSearch; _rootFolder = rootFolder; _logger = logger; + _configService = configService; } @@ -70,15 +76,84 @@ namespace NzbDrone.Core.NetImport public void Execute(NetImportSyncCommand message) { - var movies = FetchAndFilter(0, true); + //if there are no lists that are enabled for automatic import then dont do anything + if((_netImportFactory.GetAvailableProviders()).Where(a => ((NetImportDefinition)a.Definition).EnableAuto).Empty()) + { + _logger.Info("No lists are enabled for auto-import."); + return; + } + + var listedMovies = Fetch(0, true); + if (_configService.ListSyncLevel != "disabled") + { + var moviesInLibrary = _movieService.GetAllMovies(); + foreach (var movie in moviesInLibrary) + { + bool foundMatch = false; + foreach (var listedMovie in listedMovies) + { + if (movie.ImdbId == listedMovie.ImdbId) + { + foundMatch = true; + break; + } + + } + if (!foundMatch) + { + switch(_configService.ListSyncLevel) + { + case "logOnly": + _logger.Info("{0} was in your library, but not found in your lists --> You might want to unmonitor or remove it", movie); + break; + case "keepAndUnmonitor": + _logger.Info("{0} was in your library, but not found in your lists --> Keeping in library but Unmonitoring it", movie); + movie.Monitored = false; + break; + case "removeAndKeep": + _logger.Info("{0} was in your library, but not found in your lists --> Removing from library (keeping files)", movie); + _movieService.DeleteMovie(movie.Id, false); + break; + case "removeAndDelete": + _logger.Info("{0} was in your library, but not found in your lists --> Removing from library and deleting files", movie); + _movieService.DeleteMovie(movie.Id, true); + //TODO: for some reason the files are not deleted in this case... any idea why? + break; + default: + break; + } + } + } + } + + List importExclusions = null; + if (_configService.ImportExclusions != String.Empty) + { + importExclusions = _configService.ImportExclusions.Split(',').ToList(); + } + + var movies = listedMovies.Where(x => !_movieService.MovieExists(x)).ToList(); _logger.Debug("Found {0} movies on your auto enabled lists not in your library", movies.Count); foreach (var movie in movies) { - var mapped = _movieSearch.MapMovieToTmdbMovie(movie); + bool shouldAdd = true; + if (importExclusions != null) + { + foreach (var exclusion in importExclusions) + { + if (exclusion == movie.ImdbId || exclusion == movie.TmdbId.ToString()) + { + _logger.Info("Movie: {0} was found but will not be added because it {exclusion} was found on your exclusion list", exclusion); + shouldAdd = false; + break; + } + } + } - if (mapped != null) + var mapped = _movieSearch.MapMovieToTmdbMovie(movie); + if ((mapped != null) && shouldAdd) { _movieService.AddMovie(mapped); } diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs index d6d47f0e6..3d7dd2688 100644 --- a/src/NzbDrone.Core/Tv/MovieService.cs +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -33,8 +33,8 @@ namespace NzbDrone.Core.Tv Movie GetMovieByFileId(int fileId); List GetMoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); PagingSpec MoviesWithoutFiles(PagingSpec pagingSpec); - void DeleteMovie(int movieId, bool deleteFiles); void SetFileId(Movie movie, MovieFile movieFile); + void DeleteMovie(int movieId, bool deleteFiles, bool addExclusion = false); List GetAllMovies(); Movie UpdateMovie(Movie movie); List UpdateMovie(List movie); @@ -51,6 +51,7 @@ namespace NzbDrone.Core.Tv private readonly IConfigService _configService; private readonly IEventAggregator _eventAggregator; private readonly IBuildFileNames _fileNameBuilder; + private readonly IConfigService _configService; private readonly Logger _logger; public MovieService(IMovieRepository movieRepository, @@ -62,7 +63,7 @@ namespace NzbDrone.Core.Tv Logger logger) { _movieRepository = movieRepository; - _eventAggregator = eventAggregator; + _eventAggregator = eventAggregator; _fileNameBuilder = fileNameBuilder; _configService = configService; _logger = logger; @@ -237,9 +238,47 @@ namespace NzbDrone.Core.Tv return _movieRepository.FindByTitle(title.CleanSeriesTitle(), year); } - public void DeleteMovie(int movieId, bool deleteFiles) + public void DeleteMovie(int movieId, bool deleteFiles, bool addExclusion = false) { var movie = _movieRepository.Get(movieId); + if (addExclusion) + { + if (_configService.ImportExclusions.Empty()) + { + _configService.ImportExclusions = movie.ImdbId; + } + else if (!_configService.ImportExclusions.Contains(movie.ImdbId) && !_configService.ImportExclusions.Contains(movie.TmdbId.ToString())) + { + _configService.ImportExclusions += ',' + movie.ImdbId; + } + } + /*//this next block was added in order to implement listsynccleaning + //start of block -- this comment block can probably deleted in the future. just leaving here for reference + if (deleteFiles) + { + List movieFilesList = _mediaFileService.GetFilesByMovie(movieId); + //string dirPath = null; + foreach (var movieFile in movieFilesList) + { + var series = GetMovie(movieFile.MovieId); + var fullPath = Path.Combine(series.Path, movieFile.RelativePath); + //dirPath = series.Path; + _logger.Info("Deleting episode file: {0}", fullPath); + _recycleBinProvider.DeleteFile(fullPath); + _mediaFileService.Delete(movieFile, DeleteMediaFileReason.NotInList); + //TODO: files are being deleted, but empty directory left behind?? + //perhaps need to delete the series path too? + } + //if (dirPath != null) + //{ + // _logger.Info("Deleting Movie folder: {0}", dirPath); + // _recycleBinProvider.DeleteFolder(dirPath); + // _movieFileRepository.Delete(dirPath); + //} + } + //end of block + */ + _movieRepository.Delete(movieId); _eventAggregator.PublishEvent(new MovieDeletedEvent(movie, deleteFiles)); } diff --git a/src/UI/Movies/Delete/DeleteMovieTemplate.hbs b/src/UI/Movies/Delete/DeleteMovieTemplate.hbs index 1e5ce8bf7..e992e87f9 100644 --- a/src/UI/Movies/Delete/DeleteMovieTemplate.hbs +++ b/src/UI/Movies/Delete/DeleteMovieTemplate.hbs @@ -38,6 +38,30 @@
{{#if hasFile}}1{{else}}0{{/if}} movie file(s) will be deleted
+ +
+ + +
+
+
+
+
diff --git a/src/UI/Movies/Delete/DeleteMovieView.js b/src/UI/Movies/Delete/DeleteMovieView.js index 27278c7db..8c5318613 100644 --- a/src/UI/Movies/Delete/DeleteMovieView.js +++ b/src/UI/Movies/Delete/DeleteMovieView.js @@ -12,16 +12,19 @@ module.exports = Marionette.ItemView.extend({ ui : { deleteFiles : '.x-delete-files', deleteFilesInfo : '.x-delete-files-info', - indicator : '.x-indicator' + indicator : '.x-indicator', + addExclusion : '.x-add-exclusion' }, removeSeries : function() { var self = this; var deleteFiles = this.ui.deleteFiles.prop('checked'); + var addExclusion = this.ui.addExclusion.prop('checked'); this.ui.indicator.show(); - + this.model.destroy({ - data : { 'deleteFiles' : deleteFiles }, + data : { 'deleteFiles' : deleteFiles, + 'addExclusion' : addExclusion }, wait : true }).done(function() { vent.trigger(vent.Events.SeriesDeleted, { series : self.model }); diff --git a/src/UI/Settings/NetImport/Options/NetImportOptionsView.js b/src/UI/Settings/NetImport/Options/NetImportOptionsView.js index 63fd1d9cc..243d6f358 100644 --- a/src/UI/Settings/NetImport/Options/NetImportOptionsView.js +++ b/src/UI/Settings/NetImport/Options/NetImportOptionsView.js @@ -1,9 +1,70 @@ var Marionette = require('marionette'); var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); var AsValidatedView = require('../../../Mixins/AsValidatedView'); +var $ = require('jquery'); +require('../../../Mixins/TagInput'); +require('bootstrap'); +require('bootstrap.tagsinput'); var view = Marionette.ItemView.extend({ - template : 'Settings/NetImport/Options/NetImportOptionsViewTemplate' + template : 'Settings/NetImport/Options/NetImportOptionsViewTemplate', + + ui : { + importExclusions : '.x-import-exclusions' + }, + + onRender : function() { + this.ui.importExclusions.tagsinput({ + trimValue : true, + tagClass : 'label label-danger', + itemText : function(item) { + var uri; + var text; + if (item.startsWith('tt')) { + uri = window.NzbDrone.ApiRoot + '/movies/lookup/imdb?imdbId='+item; + } + else { + uri = window.NzbDrone.ApiRoot + '/movies/lookup/tmdb?tmdbId='+item; + } + var promise = $.ajax({ + url : uri, + type : 'GET', + async : false, + }); + promise.success(function(response) { + text=response['title']+' ('+response['year']+')'; + }); + + promise.error(function(request, status, error) { + text=item; + }); + return text; + } + }); + this.ui.importExclusions.on('beforeItemAdd', function(event) { + var uri; + if (event.item.startsWith('tt')) { + uri = window.NzbDrone.ApiRoot + '/movies/lookup/imdb?imdbId='+event.item; + } + else { + uri = window.NzbDrone.ApiRoot + '/movies/lookup/tmdb?tmdbId='+event.item; + } + var promise = $.ajax({ + url : uri, + type : 'GET', + async : false, + }); + promise.success(function(response) { + event.cancel=false; + }); + + promise.error(function(request, status, error) { + event.cancel = true; + window.alert(event.item+' is not a valid! Must be valid tt#### IMDB ID or #### TMDB ID'); + }); + }); + }, + }); AsModelBoundView.call(view); diff --git a/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs b/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs index 6e6072609..35f9bce5f 100644 --- a/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs +++ b/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs @@ -13,4 +13,32 @@ - +
+ +
+ + +
+ +
+ +
+
+
+ +
+ + +
+
+ + +
+
+