From 3d48da2111f006e4609893db14c8342ff4b044e1 Mon Sep 17 00:00:00 2001 From: randellhodges Date: Mon, 29 May 2017 10:56:16 -0500 Subject: [PATCH] Added HDBits Category, Codec, and Medium Filtering Capability (#1458) * Added advanced configuration options to support filtering Categories, Codecs, and Medium to the HDBits indexer. * Changes to use the existing tags with a controlled vocabulary. * 1) Sorting select options by name 2) Moved the autocomplete tag code into taginput as requested * removed commented out line * require cleanups --- .../ClientSchema/SchemaBuilder.cs | 6 +- .../Indexers/HDBits/HDBitsRequestGenerator.cs | 4 + .../Indexers/HDBits/HDBitsSettings.cs | 20 ++- src/UI/Form/TagTemplate.hbs | 4 +- src/UI/Handlebars/Helpers/String.js | 6 +- src/UI/Mixins/TagInput.js | 130 ++++++++++++------ .../Settings/Indexers/Edit/IndexerEditView.js | 11 +- 7 files changed, 129 insertions(+), 52 deletions(-) diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs index 6d731620c..af546147f 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Api.ClientSchema field.Value = value; } - if (fieldAttribute.Type == FieldType.Select) + if (fieldAttribute.Type == FieldType.Select || fieldAttribute.Type == FieldType.Tag) { field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions); } @@ -150,7 +150,7 @@ namespace NzbDrone.Api.ClientSchema private static List GetSelectOptions(Type selectOptions) { - if (selectOptions == typeof(Profile)) + if (selectOptions == null || selectOptions == typeof(Profile)) { return new List(); } @@ -165,7 +165,7 @@ namespace NzbDrone.Api.ClientSchema var options = from Enum e in Enum.GetValues(selectOptions) select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() }; - return options.OrderBy(o => o.Value).ToList(); + return options.OrderBy(o => o.Name).ToList(); } } } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index 555652a39..84b721997 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -59,6 +59,10 @@ namespace NzbDrone.Core.Indexers.HDBits query.Username = Settings.Username; query.Passkey = Settings.ApiKey; + query.Category = Settings.Categories.ToArray(); + query.Codec = Settings.Codecs.ToArray(); + query.Medium = Settings.Mediums.ToArray(); + // Require Internal only if came from RSS sync if (Settings.RequireInternal && query.ImdbInfo == null) { diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs index 62bea2a68..38bfb7003 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs @@ -1,7 +1,12 @@ -using FluentValidation; +using System; +using System.Linq; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; +using System.Linq.Expressions; +using FluentValidation.Results; +using System.Collections.Generic; namespace NzbDrone.Core.Indexers.HDBits { @@ -21,6 +26,10 @@ namespace NzbDrone.Core.Indexers.HDBits public HDBitsSettings() { BaseUrl = "https://hdbits.org"; + + Categories = new int[] { (int)HdBitsCategory.Movie }; + Codecs = new int[0]; + Mediums = new int[0]; } [FieldDefinition(0, Label = "Username")] @@ -38,6 +47,15 @@ namespace NzbDrone.Core.Indexers.HDBits [FieldDefinition(4, Label = "Require Internal", Type = FieldType.Checkbox, HelpText = "Require Internal releases for release to be accepted.")] public bool RequireInternal { get; set; } + [FieldDefinition(5, Label = "Categories", Type = FieldType.Tag, SelectOptions = typeof(HdBitsCategory), Advanced = true, HelpText = "Options: Movie, TV, Documentary, Music, Sport, Audio, XXX, MiscDemo. If unspecified, all options are used.")] + public IEnumerable Categories { get; set; } + + [FieldDefinition(6, Label = "Codecs", Type = FieldType.Tag, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "Options: h264, Mpeg2, VC1, Xvid. If unspecified, all options are used.")] + public IEnumerable Codecs { get; set; } + + [FieldDefinition(7, Label = "Mediums", Type = FieldType.Tag, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "Options: BluRay, Encode, Capture, Remux, WebDL. If unspecified, all options are used.")] + public IEnumerable Mediums { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/UI/Form/TagTemplate.hbs b/src/UI/Form/TagTemplate.hbs index 4df3ca6ba..cba8053ab 100644 --- a/src/UI/Form/TagTemplate.hbs +++ b/src/UI/Form/TagTemplate.hbs @@ -1,8 +1,8 @@ -
+
- +
{{> FormHelpPartial}} diff --git a/src/UI/Handlebars/Helpers/String.js b/src/UI/Handlebars/Helpers/String.js index 761f565c0..1da198e74 100644 --- a/src/UI/Handlebars/Helpers/String.js +++ b/src/UI/Handlebars/Helpers/String.js @@ -1,7 +1,11 @@ -var Handlebars = require('handlebars'); +var Handlebars = require('handlebars'); Handlebars.registerHelper('TitleCase', function(input) { return new Handlebars.SafeString(input.replace(/\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); })); +}); + +Handlebars.registerHelper('json', function (obj) { + return JSON.stringify(obj); }); \ No newline at end of file diff --git a/src/UI/Mixins/TagInput.js b/src/UI/Mixins/TagInput.js index 0f6a542b4..47fcd9296 100644 --- a/src/UI/Mixins/TagInput.js +++ b/src/UI/Mixins/TagInput.js @@ -1,14 +1,14 @@ -var $ = require('jquery'); +var $ = require('jquery'); var _ = require('underscore'); var TagCollection = require('../Tags/TagCollection'); var TagModel = require('../Tags/TagModel'); require('bootstrap.tagsinput'); -var substringMatcher = function(tagCollection) { +var substringMatcher = function(tags, selector) { return function findMatches (q, cb) { q = q.replace(/[^-_a-z0-9]/gi, '').toLowerCase(); - var matches = _.select(tagCollection.toJSON(), function(tag) { - return tag.label.toLowerCase().indexOf(q) > -1; + var matches = _.select(tags, function(tag) { + return selector(tag).toLowerCase().indexOf(q) > -1; }); cb(matches); }; @@ -108,49 +108,91 @@ $.fn.tagsinput.Constructor.prototype.build = function(options) { }; $.fn.tagInput = function(options) { - options = $.extend({}, { allowNew : true }, options); - var input = this; - var model = options.model; - var property = options.property; + this.each(function () { - var tagInput = $(this).tagsinput({ - tagCollection : TagCollection, - freeInput : true, - allowNew : options.allowNew, - itemValue : 'id', - itemText : 'label', - trimValue : true, - typeaheadjs : { - name : 'tags', - displayKey : 'label', - source : substringMatcher(TagCollection) + var input = $(this); + var tagInput = null; + + if (input[0].hasAttribute('tag-source')) { + + var listItems = JSON.parse(input.attr('tag-source')); + + tagInput = input.tagsinput({ + freeInput: false, + allowNew: false, + allowDuplicates: false, + itemValue: 'value', + itemText: 'name', + typeaheadjs: { + displayKey: 'name', + source: substringMatcher(listItems, function (t) { return t.name; }) + } + }); + + var origValue = input.val(); + + input.tagsinput('removeAll'); + + if (origValue) { + _.each(origValue.split(','), function (v) { + var parsed = parseInt(v); + var found = _.find(listItems, function (t) { return t.value === parsed; }); + + if (found) { + input.tagsinput('add', found); + } + }); + } } + else { + + options = $.extend({}, { allowNew: true }, options); + + var model = options.model; + var property = options.property; + + tagInput = input.tagsinput({ + tagCollection: TagCollection, + freeInput: true, + allowNew: options.allowNew, + itemValue: 'id', + itemText: 'label', + trimValue: true, + typeaheadjs: { + name: 'tags', + displayKey: 'label', + source: substringMatcher(TagCollection.toJSON(), function (t) { return t.label; }) + } + }); + + //Override the free input being set to false because we're using objects + $(tagInput)[0].options.freeInput = true; + + if (model) { + var tags = getExistingTags(model.get(property)); + + //Remove any existing tags and re-add them + input.tagsinput('removeAll'); + _.each(tags, function (tag) { + input.tagsinput('add', tag); + }); + input.tagsinput('refresh'); + input.on('itemAdded', function (event) { + var tags = model.get(property); + tags.push(event.item.id); + model.set(property, tags); + }); + input.on('itemRemoved', function (event) { + if (!event.item) { + return; + } + var tags = _.without(model.get(property), event.item.id); + model.set(property, tags); + }); + } + } + }); - //Override the free input being set to false because we're using objects - $(tagInput)[0].options.freeInput = true; - - if (model) { - var tags = getExistingTags(model.get(property)); - - //Remove any existing tags and re-add them - $(this).tagsinput('removeAll'); - _.each(tags, function(tag) { - $(input).tagsinput('add', tag); - }); - $(this).tagsinput('refresh'); - $(this).on('itemAdded', function(event) { - var tags = model.get(property); - tags.push(event.item.id); - model.set(property, tags); - }); - $(this).on('itemRemoved', function(event) { - if (!event.item) { - return; - } - var tags = _.without(model.get(property), event.item.id); - model.set(property, tags); - }); - } }; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditView.js b/src/UI/Settings/Indexers/Edit/IndexerEditView.js index 616c863a7..13245858c 100644 --- a/src/UI/Settings/Indexers/Edit/IndexerEditView.js +++ b/src/UI/Settings/Indexers/Edit/IndexerEditView.js @@ -1,4 +1,4 @@ -var _ = require('underscore'); +var _ = require('underscore'); var $ = require('jquery'); var vent = require('vent'); var Marionette = require('marionette'); @@ -8,11 +8,16 @@ var AsValidatedView = require('../../../Mixins/AsValidatedView'); var AsEditModalView = require('../../../Mixins/AsEditModalView'); require('../../../Form/FormBuilder'); require('../../../Mixins/AutoComplete'); +require('../../../Mixins/TagInput'); require('bootstrap'); var view = Marionette.ItemView.extend({ template : 'Settings/Indexers/Edit/IndexerEditViewTemplate', + ui: { + tags : '.x-form-tag' + }, + events : { 'click .x-back' : '_back', 'click .x-captcha-refresh' : '_onRefreshCaptcha' @@ -24,6 +29,10 @@ var view = Marionette.ItemView.extend({ this.targetCollection = options.targetCollection; }, + onRender: function () { + this.ui.tags.tagInput({}); + }, + _onAfterSave : function() { this.targetCollection.add(this.model, { merge : true }); vent.trigger(vent.Commands.CloseModalCommand);