New: Added filter by tag to iCal feed.

closes  #1430
This commit is contained in:
Taloth Saldono 2016-09-21 21:34:55 +02:00
parent e76fb8c90b
commit 36a3e86882
6 changed files with 120 additions and 46 deletions

View File

@ -5,17 +5,21 @@ using System.Linq;
using DDay.iCal; using DDay.iCal;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using Nancy.Responses; using Nancy.Responses;
using NzbDrone.Core.Tags;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Api.Calendar namespace NzbDrone.Api.Calendar
{ {
public class CalendarFeedModule : NzbDroneFeedModule public class CalendarFeedModule : NzbDroneFeedModule
{ {
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly ITagService _tagService;
public CalendarFeedModule(IEpisodeService episodeService) public CalendarFeedModule(IEpisodeService episodeService, ITagService tagService)
: base("calendar") : base("calendar")
{ {
_episodeService = episodeService; _episodeService = episodeService;
_tagService = tagService;
Get["/NzbDrone.ics"] = options => GetCalendarFeed(); Get["/NzbDrone.ics"] = options => GetCalendarFeed();
} }
@ -28,6 +32,7 @@ namespace NzbDrone.Api.Calendar
var end = DateTime.Today.AddDays(futureDays); var end = DateTime.Today.AddDays(futureDays);
var unmonitored = false; var unmonitored = false;
var premiersOnly = false; var premiersOnly = false;
var tags = new List<int>();
// TODO: Remove start/end parameters in v3, they don't work well for iCal // TODO: Remove start/end parameters in v3, they don't work well for iCal
var queryStart = Request.Query.Start; var queryStart = Request.Query.Start;
@ -36,6 +41,7 @@ namespace NzbDrone.Api.Calendar
var queryFutureDays = Request.Query.FutureDays; var queryFutureDays = Request.Query.FutureDays;
var queryUnmonitored = Request.Query.Unmonitored; var queryUnmonitored = Request.Query.Unmonitored;
var queryPremiersOnly = Request.Query.PremiersOnly; var queryPremiersOnly = Request.Query.PremiersOnly;
var queryTags = Request.Query.Tags;
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
@ -62,6 +68,12 @@ namespace NzbDrone.Api.Calendar
premiersOnly = bool.Parse(queryPremiersOnly.Value); 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 episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored);
var icalCalendar = new iCalendar(); var icalCalendar = new iCalendar();
@ -72,6 +84,11 @@ namespace NzbDrone.Api.Calendar
continue; continue;
} }
if (tags.Any() && tags.None(episode.Series.Tags.Contains))
{
continue;
}
var occurrence = icalCalendar.Create<Event>(); var occurrence = icalCalendar.Create<Event>();
occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString(); occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString();
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;

View File

@ -1,11 +1,13 @@
using NzbDrone.Core.Datastore; using System;
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Tags namespace NzbDrone.Core.Tags
{ {
public interface ITagRepository : IBasicRepository<Tag> public interface ITagRepository : IBasicRepository<Tag>
{ {
Tag GetByLabel(string label);
} }
public class TagRepository : BasicRepository<Tag>, ITagRepository public class TagRepository : BasicRepository<Tag>, ITagRepository
@ -14,5 +16,17 @@ namespace NzbDrone.Core.Tags
: base(database, eventAggregator) : 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;
}
} }
} }

View File

@ -8,6 +8,7 @@ namespace NzbDrone.Core.Tags
public interface ITagService public interface ITagService
{ {
Tag GetTag(int tagId); Tag GetTag(int tagId);
Tag GetTag(string tag);
List<Tag> All(); List<Tag> All();
Tag Add(Tag tag); Tag Add(Tag tag);
Tag Update(Tag tag); Tag Update(Tag tag);
@ -30,6 +31,18 @@ namespace NzbDrone.Core.Tags
return _repo.Get(tagId); 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<Tag> All() public List<Tag> All()
{ {
return _repo.All().OrderBy(t => t.Label).ToList(); return _repo.All().OrderBy(t => t.Label).ToList();

View File

@ -1,6 +1,7 @@
var Marionette = require('marionette'); var Marionette = require('marionette');
var StatusModel = require('../System/StatusModel'); var StatusModel = require('../System/StatusModel');
require('../Mixins/CopyToClipboard'); require('../Mixins/CopyToClipboard');
require('../Mixins/TagInput');
module.exports = Marionette.Layout.extend({ module.exports = Marionette.Layout.extend({
template : 'Calendar/CalendarFeedViewTemplate', template : 'Calendar/CalendarFeedViewTemplate',
@ -8,6 +9,7 @@ module.exports = Marionette.Layout.extend({
ui : { ui : {
includeUnmonitored : '.x-includeUnmonitored', includeUnmonitored : '.x-includeUnmonitored',
premiersOnly : '.x-premiersOnly', premiersOnly : '.x-premiersOnly',
tags : '.x-tags',
icalUrl : '.x-ical-url', icalUrl : '.x-ical-url',
icalCopy : '.x-ical-copy', icalCopy : '.x-ical-copy',
icalWebCal : '.x-ical-webcal' icalWebCal : '.x-ical-webcal'
@ -15,12 +17,15 @@ module.exports = Marionette.Layout.extend({
events : { events : {
'click .x-includeUnmonitored' : '_updateUrl', 'click .x-includeUnmonitored' : '_updateUrl',
'click .x-premiersOnly' : '_updateUrl' 'click .x-premiersOnly' : '_updateUrl',
'itemAdded .x-tags' : '_updateUrl',
'itemRemoved .x-tags' : '_updateUrl'
}, },
onShow : function() { onShow : function() {
this._updateUrl(); this._updateUrl();
this.ui.icalCopy.copyToClipboard(this.ui.icalUrl); this.ui.icalCopy.copyToClipboard(this.ui.icalUrl);
this.ui.tags.tagInput({ allowNew: false });
}, },
_updateUrl : function() { _updateUrl : function() {
@ -34,6 +39,10 @@ module.exports = Marionette.Layout.extend({
icalUrl += 'premiersOnly=true&'; icalUrl += 'premiersOnly=true&';
} }
if (this.ui.tags.val()) {
icalUrl += 'tags=' + this.ui.tags.val() + '&';
}
icalUrl += 'apikey=' + window.NzbDrone.ApiKey; icalUrl += 'apikey=' + window.NzbDrone.ApiKey;
var icalHttpUrl = window.location.protocol + '//' + icalUrl; var icalHttpUrl = window.location.protocol + '//' + icalUrl;

View File

@ -41,6 +41,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-sm-3 control-label">Tags</label>
<div class="col-sm-1 col-sm-push-5 help-inline">
<i class="icon-sonarr-form-info" title="One or more tags only show matching series" />
</div>
<div class="col-sm-5 col-sm-pull-1">
<input type="text" class="form-control x-tags">
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-3 control-label">iCal feed</label> <label class="col-sm-3 control-label">iCal feed</label>
<div class="col-sm-1 col-sm-push-8 help-inline"> <div class="col-sm-1 col-sm-push-8 help-inline">

View File

@ -4,10 +4,11 @@ var TagCollection = require('../Tags/TagCollection');
var TagModel = require('../Tags/TagModel'); var TagModel = require('../Tags/TagModel');
require('bootstrap.tagsinput'); require('bootstrap.tagsinput');
var substringMatcher = function() { var substringMatcher = function(tagCollection) {
return function findMatches (q, cb) { return function findMatches (q, cb) {
var matches = _.select(TagCollection.toJSON(), function(tag) { q = q.replace(/[^-_a-z0-9]/gi, '').toLowerCase();
return tag.label.toLowerCase().indexOf(q.toLowerCase()) > -1; var matches = _.select(tagCollection.toJSON(), function(tag) {
return tag.label.toLowerCase().indexOf(q) > -1;
}); });
cb(matches); cb(matches);
}; };
@ -33,22 +34,27 @@ var originalRemove = $.fn.tagsinput.Constructor.prototype.remove;
var originalBuild = $.fn.tagsinput.Constructor.prototype.build; var originalBuild = $.fn.tagsinput.Constructor.prototype.build;
$.fn.tagsinput.Constructor.prototype.add = function(item, dontPushVal) { $.fn.tagsinput.Constructor.prototype.add = function(item, dontPushVal) {
var tagCollection = this.options.tagCollection;
if (!tagCollection) {
originalAdd.call(this, item, dontPushVal);
return;
}
var self = this; var self = this;
if (typeof item === 'string' && this.options.tag) { if (typeof item === 'string') {
var test = testTag(item); var existing = _.find(tagCollection.toJSON(), { label : item });
if (existing) {
originalAdd.call(this, existing, dontPushVal);
} else if (this.options.allowNew) {
if (item === null || item === '' || !testTag(item)) { if (item === null || item === '' || !testTag(item)) {
return; return;
} }
var existing = _.find(TagCollection.toJSON(), { label : item });
if (existing) {
originalAdd.call(this, existing, dontPushVal);
} else {
var newTag = new TagModel(); var newTag = new TagModel();
newTag.set({ label : item.toLowerCase() }); newTag.set({ label : item.toLowerCase() });
TagCollection.add(newTag); tagCollection.add(newTag);
newTag.save().done(function() { newTag.save().done(function() {
item = newTag.toJSON(); item = newTag.toJSON();
@ -56,12 +62,10 @@ $.fn.tagsinput.Constructor.prototype.add = function(item, dontPushVal) {
}); });
} }
} else { } 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) { $.fn.tagsinput.Constructor.prototype.remove = function(item, dontPushVal) {
@ -104,27 +108,32 @@ $.fn.tagsinput.Constructor.prototype.build = function(options) {
}; };
$.fn.tagInput = function(options) { $.fn.tagInput = function(options) {
options = $.extend({}, { allowNew : true }, options);
var input = this; var input = this;
var model = options.model; var model = options.model;
var property = options.property; var property = options.property;
var tags = getExistingTags(model.get(property));
var tagInput = $(this).tagsinput({ var tagInput = $(this).tagsinput({
tag : true, tagCollection : TagCollection,
freeInput : true, freeInput : true,
allowNew : options.allowNew,
itemValue : 'id', itemValue : 'id',
itemText : 'label', itemText : 'label',
trimValue : true, trimValue : true,
typeaheadjs : { typeaheadjs : {
name : 'tags', name : 'tags',
displayKey : 'label', displayKey : 'label',
source : substringMatcher() source : substringMatcher(TagCollection)
} }
}); });
//Override the free input being set to false because we're using objects //Override the free input being set to false because we're using objects
$(tagInput)[0].options.freeInput = true; $(tagInput)[0].options.freeInput = true;
if (model) {
var tags = getExistingTags(model.get(property));
//Remove any existing tags and re-add them //Remove any existing tags and re-add them
$(this).tagsinput('removeAll'); $(this).tagsinput('removeAll');
_.each(tags, function(tag) { _.each(tags, function(tag) {
@ -143,4 +152,5 @@ $.fn.tagInput = function(options) {
var tags = _.without(model.get(property), event.item.id); var tags = _.without(model.get(property), event.item.id);
model.set(property, tags); model.set(property, tags);
}); });
}
}; };