From fc1585e900f8bddad4488a96b6c012c6a06f460a Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 9 May 2017 20:44:07 +0200 Subject: [PATCH] Completely overhauled how import exclusions work. Currently new exclusions can only be added when adding new movies or deleting old ones. Not manually in the settings menu. Movies can now be hidden in the new discover feature by using the new import exclusions! --- .../NetImport/ImportExclusionsModule.cs | 45 +++++++++ .../NetImport/ImportExclusionsResource.cs | 46 +++++++++ src/NzbDrone.Api/NzbDrone.Api.csproj | 2 + .../Extensions/IEnumerableExtensions.cs | 27 +++++- .../137_add_import_exclusions_table.cs | 16 ++- src/NzbDrone.Core/Datastore/TableMapping.cs | 3 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 15 ++- .../ImportExclusions/ImportExclusion.cs | 23 +++++ .../ImportExclusionsRepository.cs | 35 +++++++ .../ImportExclusionsService.cs | 73 ++++++++++++++ .../NetImport/NetImportSearchService.cs | 15 ++- src/NzbDrone.Core/NzbDrone.Core.csproj | 6 ++ src/NzbDrone.Core/Tv/MovieService.cs | 13 +-- src/UI/AddMovies/AddMoviesView.js | 25 ++--- src/UI/AddMovies/DiscoverEmptyView.js | 5 + .../AddMovies/DiscoverEmptyViewTemplate.hbs | 6 ++ src/UI/AddMovies/SearchResultView.js | 9 ++ src/UI/AddMovies/SearchResultViewTemplate.hbs | 4 + src/UI/Content/icons.less | 4 + .../Settings/NetImport/DeleteExclusionCell.js | 24 +++++ .../Settings/NetImport/ExclusionTitleCell.js | 18 ++++ .../NetImport/ImportExclusionModel.js | 7 ++ .../NetImport/ImportExclusionsCollection.js | 9 ++ src/UI/Settings/NetImport/NetImportLayout.js | 48 +++++++++ .../NetImport/NetImportLayoutTemplate.hbs | 5 + .../NetImport/Options/NetImportOptionsView.js | 97 +++++-------------- .../Options/NetImportOptionsViewTemplate.hbs | 9 +- src/UI/Settings/SettingsLayout.js | 1 + 28 files changed, 479 insertions(+), 111 deletions(-) create mode 100644 src/NzbDrone.Api/NetImport/ImportExclusionsModule.cs create mode 100644 src/NzbDrone.Api/NetImport/ImportExclusionsResource.cs create mode 100644 src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusion.cs create mode 100644 src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusionsRepository.cs create mode 100644 src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusionsService.cs create mode 100644 src/UI/AddMovies/DiscoverEmptyView.js create mode 100644 src/UI/AddMovies/DiscoverEmptyViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/DeleteExclusionCell.js create mode 100644 src/UI/Settings/NetImport/ExclusionTitleCell.js create mode 100644 src/UI/Settings/NetImport/ImportExclusionModel.js create mode 100644 src/UI/Settings/NetImport/ImportExclusionsCollection.js diff --git a/src/NzbDrone.Api/NetImport/ImportExclusionsModule.cs b/src/NzbDrone.Api/NetImport/ImportExclusionsModule.cs new file mode 100644 index 000000000..c4e1d995d --- /dev/null +++ b/src/NzbDrone.Api/NetImport/ImportExclusionsModule.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Api.ClientSchema; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.NetImport.ImportExclusions; +using NzbDrone.Core.Validation.Paths; + +namespace NzbDrone.Api.NetImport +{ + public class ImportExclusionsModule : NzbDroneRestModule + { + private readonly IImportExclusionsService _exclusionService; + + public ImportExclusionsModule(NetImportFactory netImportFactory, IImportExclusionsService exclusionService) : base("exclusions") + { + _exclusionService = exclusionService; + GetResourceAll = GetAll; + CreateResource = AddExclusion; + DeleteResource = RemoveExclusion; + GetResourceById = GetById; + } + + public List GetAll() + { + return _exclusionService.GetAllExclusions().ToResource(); + } + + public ImportExclusionsResource GetById(int id) + { + return _exclusionService.GetById(id).ToResource(); + } + + public int AddExclusion(ImportExclusionsResource exclusionResource) + { + var model = exclusionResource.ToModel(); + + return _exclusionService.AddExclusion(model).Id; + } + + public void RemoveExclusion (int id) + { + _exclusionService.RemoveExclusion(new ImportExclusion { Id = id }); + } + } +} diff --git a/src/NzbDrone.Api/NetImport/ImportExclusionsResource.cs b/src/NzbDrone.Api/NetImport/ImportExclusionsResource.cs new file mode 100644 index 000000000..a3cab77a7 --- /dev/null +++ b/src/NzbDrone.Api/NetImport/ImportExclusionsResource.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.NetImport +{ + public class ImportExclusionsResource : ProviderResource + { + //public int Id { get; set; } + public int TmdbId { get; set; } + public string MovieTitle { get; set; } + public int MovieYear { get; set; } + } + + public static class ImportExclusionsResourceMapper + { + public static ImportExclusionsResource ToResource(this Core.NetImport.ImportExclusions.ImportExclusion model) + { + if (model == null) return null; + + return new ImportExclusionsResource + { + Id = model.Id, + TmdbId = model.TmdbId, + MovieTitle = model.MovieTitle, + MovieYear = model.MovieYear + }; + } + + public static List ToResource(this IEnumerable exclusions) + { + return exclusions.Select(ToResource).ToList(); + } + + public static Core.NetImport.ImportExclusions.ImportExclusion ToModel(this ImportExclusionsResource resource) + { + return new Core.NetImport.ImportExclusions.ImportExclusion + { + TmdbId = resource.TmdbId, + MovieTitle = resource.MovieTitle, + MovieYear = resource.MovieYear + }; + } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 01e5fcb23..2e44197df 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -271,6 +271,8 @@ + + diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs index a1beecaa9..b6fca0ea2 100644 --- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -80,5 +80,30 @@ namespace NzbDrone.Common.Extensions { return source.Select(predicate).ToList(); } + + public static IEnumerable DropLast(this IEnumerable source, int n) + { + if (source == null) + throw new ArgumentNullException("source"); + + if (n < 0) + throw new ArgumentOutOfRangeException("n", + "Argument n should be non-negative."); + + return InternalDropLast(source, n); + } + + private static IEnumerable InternalDropLast(IEnumerable source, int n) + { + Queue buffer = new Queue(n + 1); + + foreach (T x in source) + { + buffer.Enqueue(x); + + if (buffer.Count == n + 1) + yield return buffer.Dequeue(); + } + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/137_add_import_exclusions_table.cs b/src/NzbDrone.Core/Datastore/Migration/137_add_import_exclusions_table.cs index f865e5754..4fe6c3e6b 100644 --- a/src/NzbDrone.Core/Datastore/Migration/137_add_import_exclusions_table.cs +++ b/src/NzbDrone.Core/Datastore/Migration/137_add_import_exclusions_table.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Collections; using System.Linq; using System.Text.RegularExpressions; +using System.Globalization; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Datastore.Migration { @@ -16,7 +18,10 @@ namespace NzbDrone.Core.Datastore.Migration { if (!this.Schema.Schema("dbo").Table("ImportExclusions").Exists()) { - Create.Table("ImportExclusions").WithColumn("tmdbid").AsInt64().NotNullable().Unique().PrimaryKey(); + Create.TableForModel("ImportExclusions") + .WithColumn("TmdbId").AsInt64().NotNullable().Unique().PrimaryKey() + .WithColumn("MovieTitle").AsString().Nullable() + .WithColumn("MovieYear").AsInt64().Nullable().WithDefault(0); } Execute.WithConnection(AddExisting); } @@ -27,18 +32,23 @@ namespace NzbDrone.Core.Datastore.Migration { getSeriesCmd.Transaction = tran; getSeriesCmd.CommandText = @"SELECT Key, Value FROM Config WHERE Key = 'importexclusions'"; + TextInfo textInfo = new CultureInfo("en-US", false).TextInfo; using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) { while (seriesReader.Read()) { var Key = seriesReader.GetString(0); var Value = seriesReader.GetString(1); - var importExclusions = Value.Split(',').Select(x => "(\""+Regex.Replace(x, @"^.*\-(.*)$", "$1")+"\")").ToList(); + + var importExclusions = Value.Split(',').Select(x => { + return string.Format("(\"{0}\", \"{1}\")", Regex.Replace(x, @"^.*\-(.*)$", "$1"), + textInfo.ToTitleCase(string.Join(" ", x.Split('-').DropLast(1)))); + }).ToList(); using (IDbCommand updateCmd = conn.CreateCommand()) { updateCmd.Transaction = tran; - updateCmd.CommandText = "INSERT INTO ImportExclusions (tmdbid) VALUES " + string.Join(", ", importExclusions); + updateCmd.CommandText = "INSERT INTO ImportExclusions (tmdbid, MovieTitle) VALUES " + string.Join(", ", importExclusions); updateCmd.ExecuteNonQuery(); } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 839404bf8..5ee687fe3 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -35,6 +35,7 @@ using NzbDrone.Core.Extras.Others; using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.NetImport; +using NzbDrone.Core.NetImport.ImportExclusions; namespace NzbDrone.Core.Datastore { @@ -105,6 +106,8 @@ namespace NzbDrone.Core.Datastore .Relationship() .HasOne(s => s.Profile, s => s.ProfileId) .HasOne(m => m.MovieFile, m => m.MovieFileId); + + Mapper.Entity().RegisterModel("ImportExclusions"); Mapper.Entity().RegisterModel("Episodes") diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index d52cbc4b5..2f7b5f2a6 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -16,6 +16,7 @@ using System.Threading; using NzbDrone.Core.Parser; using NzbDrone.Core.Profiles; using NzbDrone.Common.Serializer; +using NzbDrone.Core.NetImport.ImportExclusions; namespace NzbDrone.Core.MetadataSource.SkyHook { @@ -29,8 +30,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private readonly ITmdbConfigService _configService; private readonly IMovieService _movieService; private readonly IPreDBService _predbService; + private readonly IImportExclusionsService _exclusionService; - public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ITmdbConfigService configService, IMovieService movieService, IPreDBService predbService, Logger logger) + public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ITmdbConfigService configService, IMovieService movieService, + IPreDBService predbService, IImportExclusionsService exclusionService, Logger logger) { _httpClient = httpClient; _requestBuilder = requestBuilder.SkyHookTvdb; @@ -38,6 +41,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook _configService = configService; _movieService = movieService; _predbService = predbService; + _exclusionService = exclusionService; _logger = logger; } @@ -351,7 +355,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook public List DiscoverNewMovies(string action) { - string allIds = string.Join(",", _movieService.GetAllMovies().Select(m => m.TmdbId)); + var allMovies = _movieService.GetAllMovies(); + var allExclusions = _exclusionService.GetAllExclusions(); + string allIds = string.Join(",", allMovies.Select(m => m.TmdbId)); + string ignoredIds = string.Join(",", allExclusions.Select(ex => ex.TmdbId)); HttpRequest request; List results; @@ -387,7 +394,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook request.AllowAutoRedirect = true; request.Method = HttpMethod.POST; request.Headers.ContentType = "application/x-www-form-urlencoded"; - request.SetContent($"tmdbids={allIds}"); + request.SetContent($"tmdbids={allIds}&ignoredIds={ignoredIds}"); var response = _httpClient.Post>(request); @@ -399,6 +406,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook results = response.Resource; } + results = results.Where(m => allMovies.None(mo => mo.TmdbId == m.id) && allExclusions.None(ex => ex.TmdbId == m.id)).ToList(); + return results.SelectList(MapMovie); } diff --git a/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusion.cs b/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusion.cs new file mode 100644 index 000000000..1f8f1bdae --- /dev/null +++ b/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusion.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Marr.Data; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.MediaFiles; +using System.IO; + +namespace NzbDrone.Core.NetImport.ImportExclusions +{ + public class ImportExclusion : ModelBase + { + public int TmdbId { get; set; } + public string MovieTitle { get; set; } + public int MovieYear { get; set; } + + new public string ToString() + { + return string.Format("Excluded Movie: [{0}][{1} {2}]", TmdbId, MovieTitle, MovieYear); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusionsRepository.cs b/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusionsRepository.cs new file mode 100644 index 000000000..35689b679 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusionsRepository.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Datastore.Extensions; +using Marr.Data.QGen; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.RomanNumerals; +using NzbDrone.Core.Qualities; +using CoreParser = NzbDrone.Core.Parser.Parser; + +namespace NzbDrone.Core.NetImport.ImportExclusions +{ + public interface IImportExclusionsRepository : IBasicRepository + { + bool IsMovieExcluded(int tmdbid); + } + + public class ImportExclusionsRepository : BasicRepository, IImportExclusionsRepository + { + protected IMainDatabase _database; + + public ImportExclusionsRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + _database = database; + } + + public bool IsMovieExcluded(int tmdbid) + { + return Query.Where(ex => ex.TmdbId == tmdbid).Any(); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusionsService.cs b/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusionsService.cs new file mode 100644 index 000000000..34ff8125d --- /dev/null +++ b/src/NzbDrone.Core/NetImport/ImportExclusions/ImportExclusionsService.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.NetImport.ImportExclusions +{ + public interface IImportExclusionsService + { + List GetAllExclusions(); + bool IsMovieExcluded(int tmdbid); + ImportExclusion AddExclusion(ImportExclusion exclusion); + void RemoveExclusion(ImportExclusion exclusion); + ImportExclusion GetById(int id); + } + + public class ImportExclusionsService : IImportExclusionsService + { + private readonly IImportExclusionsRepository _exclusionRepository; + private readonly IConfigService _configService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + + public ImportExclusionsService(IImportExclusionsRepository exclusionRepository, + IEventAggregator eventAggregator, + IConfigService configService, + Logger logger) + { + _exclusionRepository = exclusionRepository; + _eventAggregator = eventAggregator; + _configService = configService; + _logger = logger; + } + + public ImportExclusion AddExclusion(ImportExclusion exclusion) + { + return _exclusionRepository.Insert(exclusion); + } + + public List GetAllExclusions() + { + return _exclusionRepository.All().ToList(); + } + + public bool IsMovieExcluded(int tmdbid) + { + return _exclusionRepository.IsMovieExcluded(tmdbid); + } + + public void RemoveExclusion(ImportExclusion exclusion) + { + _exclusionRepository.Delete(exclusion); + } + + public ImportExclusion GetById(int id) + { + return _exclusionRepository.Get(id); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs index 0fea4812d..b54352cef 100644 --- a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs +++ b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs @@ -13,6 +13,7 @@ using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.NetImport.ImportExclusions; namespace NzbDrone.Core.NetImport { @@ -32,11 +33,14 @@ namespace NzbDrone.Core.NetImport private readonly IConfigService _configService; private readonly ISearchForNzb _nzbSearchService; private readonly IProcessDownloadDecisions _processDownloadDecisions; + private readonly IImportExclusionsService _exclusionService; public NetImportSearchService(INetImportFactory netImportFactory, IMovieService movieService, ISearchForNewMovie movieSearch, IRootFolderService rootFolder, ISearchForNzb nzbSearchService, - IProcessDownloadDecisions processDownloadDecisions, IConfigService configService, Logger logger) + IProcessDownloadDecisions processDownloadDecisions, IConfigService configService, + IImportExclusionsService exclusionService, + Logger logger) { _netImportFactory = netImportFactory; _movieService = movieService; @@ -44,6 +48,7 @@ namespace NzbDrone.Core.NetImport _nzbSearchService = nzbSearchService; _processDownloadDecisions = processDownloadDecisions; _rootFolder = rootFolder; + _exclusionService = exclusionService; _logger = logger; _configService = configService; } @@ -119,18 +124,12 @@ namespace NzbDrone.Core.NetImport var importExclusions = new List(); - if (_configService.ImportExclusions != null) - { - // Replace `movie-title-tmdbid` with just tmdbid in exclusions - importExclusions = _configService.ImportExclusions.Split(',').Select(x => Regex.Replace(x, @"^.*\-(.*)$", "$1")).ToList(); - // listedMovies = listedMovies.Where(ah => importExclusions.Any(h => ah.TmdbId.ToString() != h)).ToList(); - } //var downloadedCount = 0; foreach (var movie in listedMovies) { var mapped = _movieSearch.MapMovieToTmdbMovie(movie); - if (mapped != null && !importExclusions.Any(x => x == mapped.TmdbId.ToString())) + if (mapped != null && !_exclusionService.IsMovieExcluded(mapped.TmdbId)) { //List decisions; mapped.AddOptions = new AddMovieOptions {SearchForMovie = true}; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 770261960..d25400946 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1281,6 +1281,9 @@ + + + @@ -1350,6 +1353,9 @@ + + + diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs index f998ab578..1d005bced 100644 --- a/src/NzbDrone.Core/Tv/MovieService.cs +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Datastore; using NzbDrone.Core.Configuration; +using NzbDrone.Core.NetImport.ImportExclusions; namespace NzbDrone.Core.Tv { @@ -51,6 +52,7 @@ namespace NzbDrone.Core.Tv private readonly IConfigService _configService; private readonly IEventAggregator _eventAggregator; private readonly IBuildFileNames _fileNameBuilder; + private readonly IImportExclusionsService _exclusionService; private readonly Logger _logger; @@ -60,12 +62,14 @@ namespace NzbDrone.Core.Tv IEpisodeService episodeService, IBuildFileNames fileNameBuilder, IConfigService configService, + IImportExclusionsService exclusionService, Logger logger) { _movieRepository = movieRepository; _eventAggregator = eventAggregator; _fileNameBuilder = fileNameBuilder; _configService = configService; + _exclusionService = exclusionService; _logger = logger; } @@ -286,14 +290,7 @@ namespace NzbDrone.Core.Tv var movie = _movieRepository.Get(movieId); if (addExclusion) { - if (_configService.ImportExclusions.Empty()) - { - _configService.ImportExclusions = movie.TitleSlug; - } - else if (!_configService.ImportExclusions.Contains(movie.TitleSlug)) - { - _configService.ImportExclusions += ',' + movie.TitleSlug; - } + _exclusionService.AddExclusion(new ImportExclusion {TmdbId = movie.TmdbId, MovieTitle = movie.Title, MovieYear = movie.Year } ); } _movieRepository.Delete(movieId); _eventAggregator.PublishEvent(new MovieDeletedEvent(movie, deleteFiles)); diff --git a/src/UI/AddMovies/AddMoviesView.js b/src/UI/AddMovies/AddMoviesView.js index c5f583803..97aad1c04 100644 --- a/src/UI/AddMovies/AddMoviesView.js +++ b/src/UI/AddMovies/AddMoviesView.js @@ -6,6 +6,7 @@ var AddMoviesCollection = require('./AddMoviesCollection'); var SearchResultCollectionView = require('./SearchResultCollectionView'); var EmptyView = require('./EmptyView'); var NotFoundView = require('./NotFoundView'); +var DiscoverEmptyView = require('./DiscoverEmptyView'); var ErrorView = require('./ErrorView'); var LoadingView = require('../Shared/LoadingView'); var FullMovieCollection = require("../Movies/FullMovieCollection"); @@ -112,14 +113,10 @@ module.exports = Marionette.Layout.extend({ if (this.isDiscover) { this.ui.searchBar.hide(); - if (FullMovieCollection.length > 0) { - this._discoverRecos(); - } else { - this.listenTo(FullMovieCollection, "sync", this._discover); - } - if (this.collection.length == 0) { + this._discoverRecos(); + /*if (this.collection.length == 0) { this.searchResult.show(new LoadingView()); - } + }*/ } }, @@ -192,8 +189,13 @@ module.exports = Marionette.Layout.extend({ _showResults : function() { if (!this.isClosed) { if (this.collection.length === 0) { - this.ui.searchBar.show(); - this.searchResult.show(new NotFoundView({ term : this.collection.term })); + if (this.isDiscover) { + this.searchResult.show(new DiscoverEmptyView()); + } else { + this.ui.searchBar.show(); + this.searchResult.show(new NotFoundView({ term : this.collection.term })); + } + } else { this.searchResult.show(this.resultCollectionView); if (!this.showingAll) { @@ -224,11 +226,10 @@ module.exports = Marionette.Layout.extend({ if (this.collection.action === action) { return } + this.collection.reset(); this.searchResult.show(new LoadingView()); this.collection.action = action; - this.collection.fetch({ - data : { action : action } - }); + this.currentSearchPromise = this.collection.fetch(); }, _discoverRecos : function() { diff --git a/src/UI/AddMovies/DiscoverEmptyView.js b/src/UI/AddMovies/DiscoverEmptyView.js new file mode 100644 index 000000000..77fc1f139 --- /dev/null +++ b/src/UI/AddMovies/DiscoverEmptyView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/DiscoverEmptyViewTemplate' +}); diff --git a/src/UI/AddMovies/DiscoverEmptyViewTemplate.hbs b/src/UI/AddMovies/DiscoverEmptyViewTemplate.hbs new file mode 100644 index 000000000..b9ae586fc --- /dev/null +++ b/src/UI/AddMovies/DiscoverEmptyViewTemplate.hbs @@ -0,0 +1,6 @@ +
+

+ No movies left to discover. Come back at another time :) +

+ +
diff --git a/src/UI/AddMovies/SearchResultView.js b/src/UI/AddMovies/SearchResultView.js index 0b67a7e7a..2840b5307 100644 --- a/src/UI/AddMovies/SearchResultView.js +++ b/src/UI/AddMovies/SearchResultView.js @@ -7,6 +7,7 @@ var Profiles = require('../Profile/ProfileCollection'); var RootFolders = require('./RootFolders/RootFolderCollection'); var RootFolderLayout = require('./RootFolders/RootFolderLayout'); var FullMovieCollection = require('../Movies/FullMovieCollection'); +var ImportExclusionModel = require("../Settings/NetImport/ImportExclusionModel"); var Config = require('../Config'); var Messenger = require('../Shared/Messenger'); var AsValidatedView = require('../Mixins/AsValidatedView'); @@ -33,6 +34,7 @@ var view = Marionette.ItemView.extend({ events : { 'click .x-add' : '_addWithoutSearch', 'click .x-add-search' : '_addAndSearch', + "click .x-ignore" : "_ignoreMovie", 'change .x-profile' : '_profileChanged', 'change .x-root-folder' : '_rootFolderChanged', 'change .x-season-folder' : '_seasonFolderChanged', @@ -239,6 +241,13 @@ var view = Marionette.ItemView.extend({ }); }, + _ignoreMovie : function() { + var exclusion = new ImportExclusionModel({tmdbId : this.model.get("tmdbId"), + movieTitle : this.model.get("title"), movieYear : this.model.get("year")}); + exclusion.save(); + this.remove(); + }, + _rootFoldersUpdated : function() { this._configureTemplateHelpers(); this.render(); diff --git a/src/UI/AddMovies/SearchResultViewTemplate.hbs b/src/UI/AddMovies/SearchResultViewTemplate.hbs index cf58e5df1..901654547 100644 --- a/src/UI/AddMovies/SearchResultViewTemplate.hbs +++ b/src/UI/AddMovies/SearchResultViewTemplate.hbs @@ -106,6 +106,10 @@ + + {{else}} diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index cbe72d3a1..a135e0182 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -304,6 +304,10 @@ .fa-icon-color(@brand-danger); } +.icon-sonarr-ignore { + .fa-icon-content(@fa-var-eye-slash); +} + .icon-sonarr-deleted { .fa-icon-content(@fa-var-trash); } diff --git a/src/UI/Settings/NetImport/DeleteExclusionCell.js b/src/UI/Settings/NetImport/DeleteExclusionCell.js new file mode 100644 index 000000000..9a8fa010e --- /dev/null +++ b/src/UI/Settings/NetImport/DeleteExclusionCell.js @@ -0,0 +1,24 @@ +var vent = require('vent'); +var Backgrid = require('backgrid'); + +module.exports = Backgrid.Cell.extend({ + className : 'delete-episode-file-cell', + + events : { + 'click' : '_onClick' + }, + + render : function() { + this.$el.empty(); + this.$el.html(''); + + return this; + }, + + _onClick : function() { + var self = this; + + this.model.destroy(); + + } +}); diff --git a/src/UI/Settings/NetImport/ExclusionTitleCell.js b/src/UI/Settings/NetImport/ExclusionTitleCell.js new file mode 100644 index 000000000..371f2ad76 --- /dev/null +++ b/src/UI/Settings/NetImport/ExclusionTitleCell.js @@ -0,0 +1,18 @@ +var NzbDroneCell = require('../../Cells/NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'exclusion-title-cell', + + render : function() { + this.$el.empty(); + var title = this.model.get("movieTitle"); + var year = this.model.get("movieYear"); + var str = title; + if (year > 1800) { + str += " ("+year+")"; + } + this.$el.html(str); + + return this; + } +}); diff --git a/src/UI/Settings/NetImport/ImportExclusionModel.js b/src/UI/Settings/NetImport/ImportExclusionModel.js new file mode 100644 index 000000000..1adb1f19d --- /dev/null +++ b/src/UI/Settings/NetImport/ImportExclusionModel.js @@ -0,0 +1,7 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); + +module.exports = Backbone.Model.extend({ + urlRoot : window.NzbDrone.ApiRoot + '/exclusions', + +}); diff --git a/src/UI/Settings/NetImport/ImportExclusionsCollection.js b/src/UI/Settings/NetImport/ImportExclusionsCollection.js new file mode 100644 index 000000000..66f911fb3 --- /dev/null +++ b/src/UI/Settings/NetImport/ImportExclusionsCollection.js @@ -0,0 +1,9 @@ +var Backbone = require('backbone'); +var NetImportModel = require('./ImportExclusionModel'); + +var ImportExclusionsCollection = Backbone.Collection.extend({ + model : NetImportModel, + url : window.NzbDrone.ApiRoot + '/exclusions', +}); + +module.exports = new ImportExclusionsCollection(); diff --git a/src/UI/Settings/NetImport/NetImportLayout.js b/src/UI/Settings/NetImport/NetImportLayout.js index def13963e..0da3d1af0 100644 --- a/src/UI/Settings/NetImport/NetImportLayout.js +++ b/src/UI/Settings/NetImport/NetImportLayout.js @@ -3,6 +3,14 @@ var NetImportCollection = require('./NetImportCollection'); var CollectionView = require('./NetImportCollectionView'); var OptionsView = require('./Options/NetImportOptionsView'); var RootFolderCollection = require('../../AddMovies/RootFolders/RootFolderCollection'); +var ImportExclusionsCollection = require('./ImportExclusionsCollection'); +var SelectAllCell = require('../../Cells/SelectAllCell'); +var DeleteExclusionCell = require('./DeleteExclusionCell'); +var ExclusionTitleCell = require("./ExclusionTitleCell"); +var _ = require('underscore'); +var vent = require('vent'); +var Backgrid = require('backgrid'); +var $ = require('jquery'); module.exports = Marionette.Layout.extend({ template : 'Settings/NetImport/NetImportLayoutTemplate', @@ -10,18 +18,58 @@ module.exports = Marionette.Layout.extend({ regions : { lists : '#x-lists-region', listOption : '#x-list-options-region', + importExclusions : "#exclusions" }, + columns: [{ + name: '', + cell: SelectAllCell, + headerCell: 'select-all', + sortable: false + }, { + name: 'tmdbId', + label: 'TMDBID', + cell: Backgrid.StringCell, + sortable: false, + }, { + name: 'movieTitle', + label: 'Title', + cell: ExclusionTitleCell, + cellValue: 'this', + }, { + name: 'this', + label: '', + cell: DeleteExclusionCell, + sortable: false, + }], + + initialize : function() { this.indexersCollection = new NetImportCollection(); this.indexersCollection.fetch(); RootFolderCollection.fetch().done(function() { RootFolderCollection.synced = true; }); + ImportExclusionsCollection.fetch().done(function() { + ImportExclusionsCollection.synced = true; + }); }, onShow : function() { + this.listenTo(ImportExclusionsCollection, "sync", this._showExclusions); + if (ImportExclusionsCollection.synced === true) { + this._showExclusions(); + } this.lists.show(new CollectionView({ collection : this.indexersCollection })); this.listOption.show(new OptionsView({ model : this.model })); + }, + + _showExclusions : function() { + this.exclusionGrid = new Backgrid.Grid({ + collection: ImportExclusionsCollection, + columns: this.columns, + className: 'table table-hover' + }); + this.importExclusions.show(this.exclusionGrid); } }); diff --git a/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs b/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs index c97943aa1..0869d3efa 100644 --- a/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs +++ b/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs @@ -1,4 +1,9 @@
+
+ Import Exclusions +
+
+
diff --git a/src/UI/Settings/NetImport/Options/NetImportOptionsView.js b/src/UI/Settings/NetImport/Options/NetImportOptionsView.js index d6d1924a4..b5a505830 100644 --- a/src/UI/Settings/NetImport/Options/NetImportOptionsView.js +++ b/src/UI/Settings/NetImport/Options/NetImportOptionsView.js @@ -1,6 +1,11 @@ var Marionette = require('marionette'); var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); var AsValidatedView = require('../../../Mixins/AsValidatedView'); +var ImportExclusionsCollection = require('./../ImportExclusionsCollection'); +var SelectAllCell = require('../../../Cells/SelectAllCell'); +var _ = require('underscore'); +var vent = require('vent'); +var Backgrid = require('backgrid'); var $ = require('jquery'); require('../../../Mixins/TagInput'); require('bootstrap'); @@ -22,10 +27,10 @@ var view = Marionette.ItemView.extend({ 'click .x-revoke-trakt-tokens' : '_revokeTraktTokens' }, - initialize : function() { + initialize : function() { }, - + onShow : function() { var params = new URLSearchParams(window.location.search); var oauth = params.get('access'); @@ -39,78 +44,25 @@ var view = Marionette.ItemView.extend({ //Config.setValue("traktRefreshToken", refresh); var tokenExpiry = Math.floor(Date.now() / 1000) + 4838400; this.ui.tokenExpiry.val(tokenExpiry).trigger('change'); // this means the token will expire in 8 weeks (4838400 seconds) - //Config.setValue("traktTokenExpiry",tokenExpiry); + //Config.setValue("traktTokenExpiry",tokenExpiry); //this.model.isSaved = false; //window.alert("Trakt Authentication Complete - Click Save to make the change take effect"); } if (this.ui.authToken.val() && this.ui.refreshToken.val()){ - this.ui.resetTokensButton.hide(); - this.ui.revokeTokensButton.show(); + this.ui.resetTokensButton.hide(); + this.ui.revokeTokensButton.show(); } else { - this.ui.resetTokensButton.show(); - this.ui.revokeTokensButton.hide(); + this.ui.resetTokensButton.show(); + this.ui.revokeTokensButton.hide(); } - + + + }, 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; - - //var newText = response['tmdbId']+'-'; - //if (event.item.startsWith('tt')) { - // newText = newText+'['+event.item+']'; - //} - event.item = response.titleSlug;//+' ('+response['year']+')-'+response['tmdbId']; - }); - - 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'); - }); - return event; - }); - }, + + }, ui : { resetTraktTokens : '.x-reset-trakt-tokens', @@ -118,7 +70,7 @@ var view = Marionette.ItemView.extend({ refreshToken : '.x-trakt-refresh-token', resetTokensButton : '.x-reset-trakt-tokens', revokeTokensButton : '.x-revoke-trakt-tokens', - tokenExpiry : '.x-trakt-token-expiry', + tokenExpiry : '.x-trakt-token-expiry', importExclusions : '.x-import-exclusions' }, @@ -131,15 +83,16 @@ var view = Marionette.ItemView.extend({ _revokeTraktTokens : function() { if (window.confirm("Log out of trakt.tv?")){ - //TODO: need to implement this: http://docs.trakt.apiary.io/#reference/authentication-oauth/revoke-token/revoke-an-access_token + //TODO: need to implement this: http://docs.trakt.apiary.io/#reference/authentication-oauth/revoke-token/revoke-an-access_token this.ui.authToken.val('').trigger('change'); - this.ui.refreshToken.val('').trigger('change'); - this.ui.tokenExpiry.val(0).trigger('change'); + this.ui.refreshToken.val('').trigger('change'); + this.ui.tokenExpiry.val(0).trigger('change'); this.ui.resetTokensButton.show(); - this.ui.revokeTokensButton.hide(); - window.alert("Logged out of Trakt.tv - Click Save to make the change take effect"); + this.ui.revokeTokensButton.hide(); + window.alert("Logged out of Trakt.tv - Click Save to make the change take effect"); } - } + }, + }); diff --git a/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs b/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs index bd429de47..03bea2134 100644 --- a/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs +++ b/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs @@ -30,17 +30,17 @@ -
+ Trakt Authentication
@@ -58,5 +58,6 @@
-
+ + diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index 8d852ea5f..c22ae8609 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -14,6 +14,7 @@ var IndexerCollection = require('./Indexers/IndexerCollection'); var IndexerSettingsModel = require('./Indexers/IndexerSettingsModel'); var NetImportSettingsModel = require("./NetImport/NetImportSettingsModel"); var NetImportCollection = require('./NetImport/NetImportCollection'); +var ImportExclusionsCollection = require('./NetImport/ImportExclusionsCollection'); var NetImportLayout = require('./NetImport/NetImportLayout'); var DownloadClientLayout = require('./DownloadClient/DownloadClientLayout'); var DownloadClientSettingsModel = require('./DownloadClient/DownloadClientSettingsModel');