diff --git a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs index b65ec3f1e..60aa37e28 100644 --- a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs @@ -5,17 +5,21 @@ using System.Linq; using DDay.iCal; using NzbDrone.Core.Tv; using Nancy.Responses; +using NzbDrone.Core.Tags; +using NzbDrone.Common.Extensions; namespace NzbDrone.Api.Calendar { public class CalendarFeedModule : NzbDroneFeedModule { private readonly IEpisodeService _episodeService; + private readonly ITagService _tagService; - public CalendarFeedModule(IEpisodeService episodeService) + public CalendarFeedModule(IEpisodeService episodeService, ITagService tagService) : base("calendar") { _episodeService = episodeService; + _tagService = tagService; Get["/NzbDrone.ics"] = options => GetCalendarFeed(); } @@ -28,6 +32,7 @@ namespace NzbDrone.Api.Calendar var end = DateTime.Today.AddDays(futureDays); var unmonitored = false; var premiersOnly = false; + var tags = new List(); // TODO: Remove start/end parameters in v3, they don't work well for iCal var queryStart = Request.Query.Start; @@ -36,6 +41,7 @@ namespace NzbDrone.Api.Calendar var queryFutureDays = Request.Query.FutureDays; var queryUnmonitored = Request.Query.Unmonitored; var queryPremiersOnly = Request.Query.PremiersOnly; + var queryTags = Request.Query.Tags; if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); @@ -62,6 +68,12 @@ namespace NzbDrone.Api.Calendar premiersOnly = bool.Parse(queryPremiersOnly.Value); } + if (queryTags.HasValue) + { + var tagInput = (string)queryTags.Value.ToString(); + tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); + } + var episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored); var icalCalendar = new iCalendar(); @@ -72,6 +84,11 @@ namespace NzbDrone.Api.Calendar continue; } + if (tags.Any() && tags.None(episode.Series.Tags.Contains)) + { + continue; + } + var occurrence = icalCalendar.Create(); occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString(); occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; diff --git a/src/NzbDrone.Core/Tags/TagRepository.cs b/src/NzbDrone.Core/Tags/TagRepository.cs index 5bbe55b09..500502843 100644 --- a/src/NzbDrone.Core/Tags/TagRepository.cs +++ b/src/NzbDrone.Core/Tags/TagRepository.cs @@ -1,11 +1,13 @@ -using NzbDrone.Core.Datastore; +using System; +using System.Linq; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Tags { public interface ITagRepository : IBasicRepository { - + Tag GetByLabel(string label); } public class TagRepository : BasicRepository, ITagRepository @@ -14,5 +16,17 @@ namespace NzbDrone.Core.Tags : base(database, eventAggregator) { } + + public Tag GetByLabel(string label) + { + var model = Query.Where(c => c.Label == label).SingleOrDefault(); + + if (model == null) + { + throw new InvalidOperationException("Didn't find tag with label " + label); + } + + return model; + } } } diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index 76b6f79d7..398c01ddb 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Tags public interface ITagService { Tag GetTag(int tagId); + Tag GetTag(string tag); List All(); Tag Add(Tag tag); Tag Update(Tag tag); @@ -30,6 +31,18 @@ namespace NzbDrone.Core.Tags return _repo.Get(tagId); } + public Tag GetTag(string tag) + { + if (tag.All(char.IsDigit)) + { + return _repo.Get(int.Parse(tag)); + } + else + { + return _repo.GetByLabel(tag); + } + } + public List All() { return _repo.All().OrderBy(t => t.Label).ToList(); diff --git a/src/UI/Calendar/CalendarFeedView.js b/src/UI/Calendar/CalendarFeedView.js index ae076ee7a..2d1bae197 100644 --- a/src/UI/Calendar/CalendarFeedView.js +++ b/src/UI/Calendar/CalendarFeedView.js @@ -1,6 +1,7 @@ var Marionette = require('marionette'); var StatusModel = require('../System/StatusModel'); require('../Mixins/CopyToClipboard'); +require('../Mixins/TagInput'); module.exports = Marionette.Layout.extend({ template : 'Calendar/CalendarFeedViewTemplate', @@ -8,6 +9,7 @@ module.exports = Marionette.Layout.extend({ ui : { includeUnmonitored : '.x-includeUnmonitored', premiersOnly : '.x-premiersOnly', + tags : '.x-tags', icalUrl : '.x-ical-url', icalCopy : '.x-ical-copy', icalWebCal : '.x-ical-webcal' @@ -15,12 +17,15 @@ module.exports = Marionette.Layout.extend({ events : { 'click .x-includeUnmonitored' : '_updateUrl', - 'click .x-premiersOnly' : '_updateUrl' + 'click .x-premiersOnly' : '_updateUrl', + 'itemAdded .x-tags' : '_updateUrl', + 'itemRemoved .x-tags' : '_updateUrl' }, onShow : function() { this._updateUrl(); this.ui.icalCopy.copyToClipboard(this.ui.icalUrl); + this.ui.tags.tagInput({ allowNew: false }); }, _updateUrl : function() { @@ -34,6 +39,10 @@ module.exports = Marionette.Layout.extend({ icalUrl += 'premiersOnly=true&'; } + if (this.ui.tags.val()) { + icalUrl += 'tags=' + this.ui.tags.val() + '&'; + } + icalUrl += 'apikey=' + window.NzbDrone.ApiKey; var icalHttpUrl = window.location.protocol + '//' + icalUrl; diff --git a/src/UI/Calendar/CalendarFeedViewTemplate.hbs b/src/UI/Calendar/CalendarFeedViewTemplate.hbs index 4c40d21b7..c192c740d 100644 --- a/src/UI/Calendar/CalendarFeedViewTemplate.hbs +++ b/src/UI/Calendar/CalendarFeedViewTemplate.hbs @@ -41,6 +41,17 @@ +
+ + +
+ +
+ +
+ +
+
diff --git a/src/UI/Mixins/TagInput.js b/src/UI/Mixins/TagInput.js index 1994e3d3b..0f6a542b4 100644 --- a/src/UI/Mixins/TagInput.js +++ b/src/UI/Mixins/TagInput.js @@ -4,10 +4,11 @@ var TagCollection = require('../Tags/TagCollection'); var TagModel = require('../Tags/TagModel'); require('bootstrap.tagsinput'); -var substringMatcher = function() { +var substringMatcher = function(tagCollection) { return function findMatches (q, cb) { - var matches = _.select(TagCollection.toJSON(), function(tag) { - return tag.label.toLowerCase().indexOf(q.toLowerCase()) > -1; + q = q.replace(/[^-_a-z0-9]/gi, '').toLowerCase(); + var matches = _.select(tagCollection.toJSON(), function(tag) { + return tag.label.toLowerCase().indexOf(q) > -1; }); cb(matches); }; @@ -33,22 +34,27 @@ var originalRemove = $.fn.tagsinput.Constructor.prototype.remove; var originalBuild = $.fn.tagsinput.Constructor.prototype.build; $.fn.tagsinput.Constructor.prototype.add = function(item, dontPushVal) { - var self = this; + var tagCollection = this.options.tagCollection; - if (typeof item === 'string' && this.options.tag) { - var test = testTag(item); - if (item === null || item === '' || !testTag(item)) { - return; - } + if (!tagCollection) { + originalAdd.call(this, item, dontPushVal); + return; + } + var self = this; - var existing = _.find(TagCollection.toJSON(), { label : item }); + if (typeof item === 'string') { + var existing = _.find(tagCollection.toJSON(), { label : item }); if (existing) { originalAdd.call(this, existing, dontPushVal); - } else { + } else if (this.options.allowNew) { + if (item === null || item === '' || !testTag(item)) { + return; + } + var newTag = new TagModel(); newTag.set({ label : item.toLowerCase() }); - TagCollection.add(newTag); + tagCollection.add(newTag); newTag.save().done(function() { item = newTag.toJSON(); @@ -56,12 +62,10 @@ $.fn.tagsinput.Constructor.prototype.add = function(item, dontPushVal) { }); } } else { - originalAdd.call(this, item, dontPushVal); + originalAdd.call(self, item, dontPushVal); } - if (this.options.tag) { - self.$input.typeahead('val', ''); - } + self.$input.typeahead('val', ''); }; $.fn.tagsinput.Constructor.prototype.remove = function(item, dontPushVal) { @@ -104,43 +108,49 @@ $.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; - var tags = getExistingTags(model.get(property)); var tagInput = $(this).tagsinput({ - tag : true, - freeInput : true, - itemValue : 'id', - itemText : 'label', - trimValue : true, - typeaheadjs : { + tagCollection : TagCollection, + freeInput : true, + allowNew : options.allowNew, + itemValue : 'id', + itemText : 'label', + trimValue : true, + typeaheadjs : { name : 'tags', displayKey : 'label', - source : substringMatcher() + source : substringMatcher(TagCollection) } }); //Override the free input being set to false because we're using objects $(tagInput)[0].options.freeInput = true; - //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); - }); + 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