parent
e76fb8c90b
commit
36a3e86882
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
Loading…
Reference in New Issue