Merge pull request #231 from Sonarr/unmonitored-calendar

Unmonitored episodes on calendar
This commit is contained in:
Mark McDowall 2015-04-14 20:37:02 -07:00
commit c43296ffe9
15 changed files with 176 additions and 47 deletions

View File

@ -31,7 +31,7 @@ namespace NzbDrone.Api.Calendar
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
var episodes = _episodeService.EpisodesBetweenDates(start, end);
var episodes = _episodeService.EpisodesBetweenDates(start, end, false);
var icalCalendar = new iCalendar();
foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value))

View File

@ -23,14 +23,17 @@ namespace NzbDrone.Api.Calendar
{
var start = DateTime.Today;
var end = DateTime.Today.AddDays(2);
var includeUnmonitored = false;
var queryStart = Request.Query.Start;
var queryEnd = Request.Query.End;
var queryIncludeUnmonitored = Request.Query.Unmonitored;
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
if (queryIncludeUnmonitored.HasValue) includeUnmonitored = Convert.ToBoolean(queryIncludeUnmonitored.Value);
var resources = ToListResource(() => _episodeService.EpisodesBetweenDates(start, end));
var resources = ToListResource(() => _episodeService.EpisodesBetweenDates(start, end, includeUnmonitored));
return resources.OrderBy(e => e.AirDateUtc).ToList();
}

View File

@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests
[Test]
public void should_get_episodes()
{
var episodes = Subject.EpisodesBetweenDates(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(3));
var episodes = Subject.EpisodesBetweenDates(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(3), false);
episodes.Should().HaveCount(1);
}
}

View File

@ -47,7 +47,7 @@ namespace NzbDrone.Core.IndexerSearch
public void MissingEpisodesAiredAfter(DateTime dateTime, IEnumerable<Int32> grabbed)
{
var missing = _episodeService.EpisodesBetweenDates(dateTime, DateTime.UtcNow)
var missing = _episodeService.EpisodesBetweenDates(dateTime, DateTime.UtcNow, false)
.Where(e => !e.HasFile &&
!_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id) &&
!grabbed.Contains(e.Id))

View File

@ -26,7 +26,7 @@ namespace NzbDrone.Core.Tv
PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber);
Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate);
List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored);
void SetMonitoredFlat(Episode episode, bool monitored);
void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored);
void SetFileId(int episodeId, int fileId);
@ -151,14 +151,20 @@ namespace NzbDrone.Core.Tv
return episodes.Single();
}
public List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate)
public List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored)
{
return Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id)
.Where<Episode>(e => e.AirDateUtc >= startDate)
.AndWhere(e => e.AirDateUtc <= endDate)
.AndWhere(e => e.Monitored)
.AndWhere(e => e.Series.Monitored)
.ToList();
var query = Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id)
.Where<Episode>(e => e.AirDateUtc >= startDate)
.AndWhere(e => e.AirDateUtc <= endDate);
if (!includeUnmonitored)
{
query.AndWhere(e => e.Monitored)
.AndWhere(e => e.Series.Monitored);
}
return query.ToList();
}
public void SetMonitoredFlat(Episode episode, bool monitored)

View File

@ -30,7 +30,7 @@ namespace NzbDrone.Core.Tv
void UpdateEpisode(Episode episode);
void SetEpisodeMonitored(int episodeId, bool monitored);
void UpdateEpisodes(List<Episode> episodes);
List<Episode> EpisodesBetweenDates(DateTime start, DateTime end);
List<Episode> EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
void InsertMany(List<Episode> episodes);
void UpdateMany(List<Episode> episodes);
void DeleteMany(List<Episode> episodes);
@ -169,9 +169,9 @@ namespace NzbDrone.Core.Tv
_episodeRepository.UpdateMany(episodes);
}
public List<Episode> EpisodesBetweenDates(DateTime start, DateTime end)
public List<Episode> EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored)
{
var episodes = _episodeRepository.EpisodesBetweenDates(start.ToUniversalTime(), end.ToUniversalTime());
var episodes = _episodeRepository.EpisodesBetweenDates(start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored);
return episodes;
}

View File

@ -2,8 +2,9 @@ var Backbone = require('backbone');
var EpisodeModel = require('../Series/EpisodeModel');
module.exports = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/calendar',
model : EpisodeModel,
url : window.NzbDrone.ApiRoot + '/calendar',
model : EpisodeModel,
tableName : 'calendar',
comparator : function(model) {
var date = new Date(model.get('airDateUtc'));

View File

@ -3,34 +3,94 @@ var Marionette = require('marionette');
var UpcomingCollectionView = require('./UpcomingCollectionView');
var CalendarView = require('./CalendarView');
var CalendarFeedView = require('./CalendarFeedView');
var ToolbarLayout = require('../Shared/Toolbar/ToolbarLayout');
module.exports = Marionette.Layout.extend({
template : 'Calendar/CalendarLayoutTemplate',
regions : {
upcoming : '#x-upcoming',
calendar : '#x-calendar'
},
events : {
'click .x-ical' : '_showiCal'
calendar : '#x-calendar',
toolbar : '#x-toolbar'
},
onShow : function() {
this._showUpcoming();
this._showCalendar();
this._showToolbar();
},
_showUpcoming : function() {
this.upcoming.show(new UpcomingCollectionView());
this.upcomingView = new UpcomingCollectionView();
this.upcoming.show(this.upcomingView);
},
_showCalendar : function() {
this.calendar.show(new CalendarView());
this.calendarView = new CalendarView();
this.calendar.show(this.calendarView);
},
_showiCal : function() {
var view = new CalendarFeedView();
AppLayout.modalRegion.show(view);
},
_showToolbar : function() {
var leftSideButtons = {
type : 'default',
storeState : false,
items : [
{
title : 'Get iCal Link',
icon : 'icon-sonarr-calendar-o',
callback : this._showiCal,
ownerContext : this
}
]
};
var filterOptions = {
type : 'radio',
storeState : true,
menuKey : 'calendar.show',
defaultAction : 'monitored',
items : [
{
key : 'all',
title : '',
tooltip : 'All',
icon : 'icon-sonarr-all',
callback : this._setCalendarFilter
},
{
key : 'monitored',
title : '',
tooltip : 'Monitored Only',
icon : 'icon-sonarr-monitored',
callback : this._setCalendarFilter
}
]
};
this.toolbar.show(new ToolbarLayout({
left : [leftSideButtons],
right : [filterOptions],
context : this,
floatOnMobile : true
}));
},
_setCalendarFilter : function(buttonContext) {
var mode = buttonContext.model.get('key');
if (mode === 'all') {
this.calendarView.setShowUnmonitored(true);
this.upcomingView.setShowUnmonitored(true);
}
else {
this.calendarView.setShowUnmonitored(false);
this.upcomingView.setShowUnmonitored(false);
}
}
});

View File

@ -3,23 +3,20 @@
<div class="pull-left">
<h4>Upcoming</h4>
</div>
<div class="pull-right">
<h4>
<i class="icon-sonarr-calendar-o ical x-ical"></i>
</h4>
</div>
<div id="x-upcoming"/>
</div>
<div class="col-md-9 col-xs-12">
<div id="x-toolbar" class="calendar-toolbar"/>
<div id="x-calendar" class="calendar"/>
<div class="legend calendar">
<ul class='legend-labels'>
<li><span class="premiere" title="Premiere episode hasn't aired yet"></span>Unaired Premiere</li>
<li><span class="primary" title="Episode hasn't aired yet"></span>Unaired</li>
<li><span class="warning" title="Episode is currently airing"></span>On Air</li>
<li><span class="purple" title="Episode is currently downloading"></span>Downloading</li>
<li><span class="danger" title="Episode file has not been found"></span>Missing</li>
<li><span class="success" title="Episode was downloaded and sorted"></span>Downloaded</li>
<li class="legend-label"><span class="premiere" title="Premiere episode hasn't aired yet"></span>Unaired Premiere</li>
<li class="legend-label"><span class="primary" title="Episode hasn't aired yet"></span>Unaired</li>
<li class="legend-label"><span class="warning" title="Episode is currently airing"></span>On Air</li>
<li class="legend-label"><span class="purple" title="Episode is currently downloading"></span>Downloading</li>
<li class="legend-label"><span class="danger" title="Episode file has not been found"></span>Missing</li>
<li class="legend-label"><span class="success" title="Episode was downloaded and sorted"></span>Downloaded</li>
<li class="legend-label"><span class="unmonitored" title="Episode is unmonitored"></span>Unmonitored</li>
</ul>
</div>
</div>

View File

@ -2,10 +2,11 @@ var $ = require('jquery');
var vent = require('vent');
var Marionette = require('marionette');
var moment = require('moment');
var CalendarCollection = require('./Collection');
var CalendarCollection = require('./CalendarCollection');
var UiSettings = require('../Shared/UiSettingsModel');
var QueueCollection = require('../Activity/Queue/QueueCollection');
var Config = require('../Config');
require('../Mixins/backbone.signalr.mixin');
require('fullcalendar');
require('jquery.easypiechart');
@ -14,6 +15,7 @@ module.exports = Marionette.ItemView.extend({
storageKey : 'calendar.view',
initialize : function() {
this.showUnmonitored = Config.getValue('calendar.show', 'monitored') === 'all';
this.collection = new CalendarCollection().bindSignalR({ updateOnly : true });
this.listenTo(this.collection, 'change', this._reloadCalendarEvents);
this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents);
@ -27,6 +29,13 @@ module.exports = Marionette.ItemView.extend({
this.$('.fc-button-today').click();
},
setShowUnmonitored : function (showUnmonitored) {
if (this.showUnmonitored !== showUnmonitored) {
this.showUnmonitored = showUnmonitored;
this._getEvents(this.$el.fullCalendar('getView'));
}
},
_viewRender : function(view) {
if ($(window).width() < 768) {
this.$('.fc-header-title').show();
@ -105,8 +114,9 @@ module.exports = Marionette.ItemView.extend({
this.collection.fetch({
data : {
start : start,
end : end
start : start,
end : end,
unmonitored : this.showUnmonitored
},
success : this._setEventData.bind(this)
});
@ -145,6 +155,7 @@ module.exports = Marionette.ItemView.extend({
var currentTime = moment();
var start = moment(element.get('airDateUtc'));
var end = moment(endTime);
var monitored = element.get('series').monitored && element.get('monitored');
var statusLevel = 'primary';
@ -156,6 +167,10 @@ module.exports = Marionette.ItemView.extend({
statusLevel = 'purple';
}
else if (!monitored) {
statusLevel = 'unmonitored';
}
else if (currentTime.isAfter(start) && currentTime.isBefore(end)) {
statusLevel = 'warning';
}
@ -231,6 +246,7 @@ module.exports = Marionette.ItemView.extend({
return options;
},
_addStatusIcon : function(element, icon, tooltip) {
this.$(element).find('.fc-event-time').after('<span class="status pull-right"><i class="{0}"></i></span>'.format(icon));
this.$(element).find('.status').tooltip({

View File

@ -2,14 +2,16 @@ var _ = require('underscore');
var Marionette = require('marionette');
var UpcomingCollection = require('./UpcomingCollection');
var UpcomingItemView = require('./UpcomingItemView');
var Config = require('../Config');
require('../Mixins/backbone.signalr.mixin');
module.exports = Marionette.CollectionView.extend({
itemView : UpcomingItemView,
initialize : function() {
this.showUnmonitored = Config.getValue('calendar.show', 'monitored') === 'all';
this.collection = new UpcomingCollection().bindSignalR({ updateOnly : true });
this.collection.fetch();
this._fetchCollection();
this._fetchCollection = _.bind(this._fetchCollection, this);
this.timer = window.setInterval(this._fetchCollection, 60 * 60 * 1000);
@ -19,7 +21,14 @@ module.exports = Marionette.CollectionView.extend({
window.clearInterval(this.timer);
},
setShowUnmonitored : function (showUnmonitored) {
if (this.showUnmonitored !== showUnmonitored) {
this.showUnmonitored = showUnmonitored;
this._fetchCollection();
}
},
_fetchCollection : function() {
this.collection.fetch();
this.collection.fetch({ data: { unmonitored : this.showUnmonitored }});
}
});

View File

@ -36,7 +36,7 @@
}
.fc-state-highlight {
background : #f1f1f1;
background : #dbdbdb;
}
.past {
@ -119,17 +119,21 @@
border-color : @droneTeal;
}
.unmonitored {
border-color : grey;
}
.episode-title {
.btn-link;
.text-overflow;
color : @link-color;
margin-top : 1px;
display : inline-block;
@media (max-width: @screen-xs-min) {
width : 140px;
}
@media (min-width: @screen-md-min) {
width : 135px;
}
@ -138,7 +142,7 @@
.calendar {
background-position : -160px -128px;
// background-position : -160px -128px;
.primary {
border-color : @btn-primary-bg;
@ -185,10 +189,28 @@
background-color : @droneTeal;
}
.unmonitored {
border-color : grey;
background-color : grey;
}
.chart {
margin-top : 2px;
margin-right: 2px;
}
.legend-labels {
width : 500px;
@media (max-width: @screen-xs-min) {
width : 400px;
}
}
.legend-label {
display : inline-block;
width : 150px;
}
}
.ical {
@ -211,3 +233,9 @@
margin-bottom : 5px;
}
}
.calendar-toolbar {
.page-toolbar {
margin-bottom : 10px;
}
}

View File

@ -20,6 +20,7 @@ Handlebars.registerHelper('StatusLevel', function() {
var currentTime = moment();
var start = moment(this.airDateUtc);
var end = moment(this.end);
var monitored = this.series.monitored && this.monitored;
if (hasFile) {
return 'success';
@ -29,6 +30,10 @@ Handlebars.registerHelper('StatusLevel', function() {
return 'purple';
}
else if (!monitored) {
return 'unmonitored';
}
if (this.episodeNumber === 1) {
return 'premiere';
}

View File

@ -24,6 +24,10 @@ module.exports = Marionette.Layout.extend({
throw 'context needs to be passed';
}
this.templateHelpers = {
floatOnMobile : options.floatOnMobile || false
};
this.left = options.left;
this.right = options.right;
this.toolbarContext = options.context;
@ -51,7 +55,7 @@ module.exports = Marionette.Layout.extend({
_.each(buttonGroup.items, function(button) {
if (buttonGroup.storeState && !button.key) {
throw 'must provide key for all buttons when storSstate is enabled';
throw 'must provide key for all buttons when storeState is enabled';
}
var model = new ButtonModel(button);

View File

@ -1,2 +1,2 @@
<div class="page-toolbar pull-left pull-none-xs x-toolbar-left" />
<div class="page-toolbar pull-right pull-none-xs x-toolbar-right" />
<div class="page-toolbar pull-left {{#unless floatOnMobile}}pull-none-xs{{/unless}} x-toolbar-left" />
<div class="page-toolbar pull-right {{#unless floatOnMobile}}pull-none-xs{{/unless}} x-toolbar-right" />