diff --git a/NzbDrone.Api/AutomapperBootstraper.cs b/NzbDrone.Api/AutomapperBootstraper.cs index 6eac1d014..e68f25fdd 100644 --- a/NzbDrone.Api/AutomapperBootstraper.cs +++ b/NzbDrone.Api/AutomapperBootstraper.cs @@ -2,6 +2,7 @@ using AutoMapper; using NzbDrone.Api.Calendar; using NzbDrone.Api.Episodes; +using NzbDrone.Api.Missing; using NzbDrone.Api.QualityProfiles; using NzbDrone.Api.QualityType; using NzbDrone.Api.Resolvers; @@ -52,6 +53,11 @@ namespace NzbDrone.Api //Episode Mapper.CreateMap(); + + //Missing + Mapper.CreateMap() + .ForMember(dest => dest.SeriesTitle, opt => opt.MapFrom(src => src.Series.Title)) + .ForMember(dest => dest.EpisodeTitle, opt => opt.MapFrom(src => src.Title)); } } } \ No newline at end of file diff --git a/NzbDrone.Api/Missing/MissingModule.cs b/NzbDrone.Api/Missing/MissingModule.cs new file mode 100644 index 000000000..354fe8eab --- /dev/null +++ b/NzbDrone.Api/Missing/MissingModule.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.Missing +{ + public class MissingModule : NzbDroneApiModule + { + private readonly EpisodeService _episodeService; + + public MissingModule(EpisodeService episodeService) + : base("/missing") + { + _episodeService = episodeService; + Get["/"] = x => GetMissingEpisodes(); + } + + private Response GetMissingEpisodes() + { + bool includeSpecials; + Boolean.TryParse(PrimitiveExtensions.ToNullSafeString(Request.Query.IncludeSpecials), out includeSpecials); + + var episodes = _episodeService.EpisodesWithoutFiles(includeSpecials); + return Mapper.Map, List>(episodes).AsResponse(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Missing/MissingResource.cs b/NzbDrone.Api/Missing/MissingResource.cs new file mode 100644 index 000000000..00a65dd96 --- /dev/null +++ b/NzbDrone.Api/Missing/MissingResource.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; + +namespace NzbDrone.Api.Missing +{ + public class MissingResource + { + public Int32 SeriesId { get; set; } + public String SeriesTitle { get; set; } + public Int32 EpisodeId { get; set; } + public String EpisodeTitle { get; set; } + public Int32 SeasonNumber { get; set; } + public Int32 EpisodeNumber { get; set; } + public DateTime? AirDate { get; set; } + public String Overview { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index 8e68466f3..e4ee3faa4 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -120,6 +120,8 @@ + + diff --git a/NzbDrone.Backbone/Controller.js b/NzbDrone.Backbone/Controller.js index 738f5bd78..8d5483237 100644 --- a/NzbDrone.Backbone/Controller.js +++ b/NzbDrone.Backbone/Controller.js @@ -3,7 +3,7 @@ 'Calendar/CalendarCollectionView', 'Shared/NotificationView', 'Shared/NotFoundView', 'MainMenuView', 'HeaderView', 'Series/Details/SeriesDetailsView', 'Series/EpisodeCollection', - 'Settings/SettingsLayout'], + 'Settings/SettingsLayout', 'Missing/MissingCollectionView'], function (app, modalRegion) { var controller = Backbone.Marionette.Controller.extend({ @@ -48,12 +48,23 @@ var settingsModel = new NzbDrone.Settings.SettingsModel(); settingsModel.fetch({ - success: function(settings){ + success: function(settings) { NzbDrone.mainRegion.show(new NzbDrone.Settings.SettingsLayout(this, action, query, settings)); } }); }, + missing: function(action, query) { + this.setTitle('Missing'); + + var missingCollection = new NzbDrone.Missing.MissingCollection(); + missingCollection.fetch({ + success: function(missing) { + NzbDrone.mainRegion.show(new NzbDrone.Missing.MissingCollectionView(this, action, query, missing)); + } + }) + }, + notFound: function () { this.setTitle('Not Found'); NzbDrone.mainRegion.show(new NzbDrone.Shared.NotFoundView(this)); diff --git a/NzbDrone.Backbone/Missing/MissingCollection.js b/NzbDrone.Backbone/Missing/MissingCollection.js new file mode 100644 index 000000000..6c7b1e9de --- /dev/null +++ b/NzbDrone.Backbone/Missing/MissingCollection.js @@ -0,0 +1,9 @@ +define(['app', 'Missing/MissingModel'], function () { + NzbDrone.Missing.MissingCollection = Backbone.Collection.extend({ + url: NzbDrone.Constants.ApiRoot + '/missing', + model: NzbDrone.Missing.MissingModel, + comparator: function(model) { + return model.get('airDate'); + } + }); +}); \ No newline at end of file diff --git a/NzbDrone.Backbone/Missing/MissingCollectionTemplate.html b/NzbDrone.Backbone/Missing/MissingCollectionTemplate.html new file mode 100644 index 000000000..b56030cbf --- /dev/null +++ b/NzbDrone.Backbone/Missing/MissingCollectionTemplate.html @@ -0,0 +1,12 @@ + + + + + + + + + + + +
Series TitleEpisodeEpisode TitleAir Date
diff --git a/NzbDrone.Backbone/Missing/MissingCollectionView.js b/NzbDrone.Backbone/Missing/MissingCollectionView.js new file mode 100644 index 000000000..14ee3cf81 --- /dev/null +++ b/NzbDrone.Backbone/Missing/MissingCollectionView.js @@ -0,0 +1,75 @@ +'use strict'; + +define(['app', 'Missing/MissingItemView'], function (app) { + NzbDrone.Missing.MissingCollectionView = Backbone.Marionette.CompositeView.extend({ + itemView: NzbDrone.Missing.MissingItemView, + itemViewContainer: 'tbody', + template: 'Missing/MissingCollectionTemplate', + + ui:{ + table : '.x-missing-table' + }, + + initialize: function (context, action, query, collection) { + this.collection = collection; + }, + onCompositeCollectionRendered: function() { + this.ui.table.trigger('update'); + + if(!this.tableSorter && this.collection.length > 0) + { + this.tableSorter = this.ui.table.tablesorter({ + textExtraction: function (node) { + return node.innerHTML; + }, + sortList: [[3,1]], + headers: { + 0: { + sorter: 'innerHtml' + }, + 1: { + sorter: false + }, + 2: { + sorter: false + }, + 3: { + sorter: 'date' + }, + 4: { + sorter: false + } + } + }); + + //Todo: We should extract these common settings out + this.ui.table.find('th.header').each(function(){ + $(this).append(''); + }); + + this.ui.table.bind("sortEnd", function() { + $(this).find('th.header i').each(function(){ + $(this).remove(); + }); + + $(this).find('th.header').each(function () { + if (!$(this).hasClass('headerSortDown') && !$(this).hasClass('headerSortUp')) + $(this).append(''); + }); + + $(this).find('th.headerSortDown').each(function(){ + $(this).append(''); + }); + + $(this).find('th.headerSortUp').each(function(){ + $(this).append(''); + }); + }); + } + else + { + this.ui.table.trigger('update'); + } + } + }); +}); \ No newline at end of file diff --git a/NzbDrone.Backbone/Missing/MissingItemTemplate.html b/NzbDrone.Backbone/Missing/MissingItemTemplate.html new file mode 100644 index 000000000..779ffee63 --- /dev/null +++ b/NzbDrone.Backbone/Missing/MissingItemTemplate.html @@ -0,0 +1,5 @@ +{{seriesTitle}} +{{seasonNumber}}x{{paddedEpisodeNumber}} + +{{bestDateString}} + \ No newline at end of file diff --git a/NzbDrone.Backbone/Missing/MissingItemView.js b/NzbDrone.Backbone/Missing/MissingItemView.js new file mode 100644 index 000000000..cd32f1f4f --- /dev/null +++ b/NzbDrone.Backbone/Missing/MissingItemView.js @@ -0,0 +1,16 @@ +'use strict'; + +define([ + 'app', + 'Missing/MissingCollection' + +], function () { + NzbDrone.Missing.MissingItemView = Backbone.Marionette.ItemView.extend({ + template: 'Missing/MissingItemTemplate', + tagName: 'tr', + + onRender: function () { + NzbDrone.ModelBinder.bind(this.model, this.el); + } + }) +}) \ No newline at end of file diff --git a/NzbDrone.Backbone/Missing/MissingModel.js b/NzbDrone.Backbone/Missing/MissingModel.js new file mode 100644 index 000000000..a674862e2 --- /dev/null +++ b/NzbDrone.Backbone/Missing/MissingModel.js @@ -0,0 +1,12 @@ +define(['app'], function (app) { + NzbDrone.Missing.MissingModel = Backbone.Model.extend({ + mutators: { + bestDateString: function () { + return bestDateString(this.get('airDate')); + }, + paddedEpisodeNumber: function(){ + return this.get('episodeNumber'); + } + } + }); +}); diff --git a/NzbDrone.Backbone/Mixins/spoon.js b/NzbDrone.Backbone/Mixins/spoon.js index 12be1f1c0..879c4d9f6 100644 --- a/NzbDrone.Backbone/Mixins/spoon.js +++ b/NzbDrone.Backbone/Mixins/spoon.js @@ -6,7 +6,7 @@ function bestDateString(sourceDate){ if (date.isYesterday()) return 'Yesterday'; if (date.isToday()) return 'Today'; if (date.isTomorrow()) return 'Tomorrow'; - if (date.isBefore(Date.create().addDays(7))) return date.format('{Weekday}'); + if (date.isAfter(Date.create('tomorrow')) && date.isBefore(Date.create().addDays(7))) return date.format('{Weekday}'); return date.format('{MM}/{dd}/{yyyy}'); } \ No newline at end of file diff --git a/NzbDrone.Backbone/Routing.js b/NzbDrone.Backbone/Routing.js index b29cc4e29..d5ad221b9 100644 --- a/NzbDrone.Backbone/Routing.js +++ b/NzbDrone.Backbone/Routing.js @@ -15,6 +15,7 @@ 'calendar': 'calendar', 'settings': 'settings', 'settings/:action(/:query)': 'settings', + 'missing': 'missing', ':whatever': 'notFound' } }); diff --git a/NzbDrone.Backbone/Series/Index/SeriesIndexCollectionView.js b/NzbDrone.Backbone/Series/Index/SeriesIndexCollectionView.js index c1a1f8893..7ce547fbf 100644 --- a/NzbDrone.Backbone/Series/Index/SeriesIndexCollectionView.js +++ b/NzbDrone.Backbone/Series/Index/SeriesIndexCollectionView.js @@ -57,6 +57,7 @@ define(['app', 'Quality/QualityProfileCollection', 'Series/Index/SeriesItemView' } }); + //Todo: We should extract these common settings out this.ui.table.find('th.header').each(function(){ $(this).append(''); }); @@ -72,11 +73,11 @@ define(['app', 'Quality/QualityProfileCollection', 'Series/Index/SeriesItemView' }); $(this).find('th.headerSortDown').each(function(){ - $(this).append(''); + $(this).append(''); }); $(this).find('th.headerSortUp').each(function(){ - $(this).append(''); + $(this).append(''); }); }); } diff --git a/NzbDrone.Backbone/app.js b/NzbDrone.Backbone/app.js index c21c2465a..5091b3b61 100644 --- a/NzbDrone.Backbone/app.js +++ b/NzbDrone.Backbone/app.js @@ -52,6 +52,7 @@ define('app', function () { window.NzbDrone.Settings.Notifications = {}; window.NzbDrone.Settings.System = {}; window.NzbDrone.Settings.Misc = {}; + window.NzbDrone.Missing = {}; window.NzbDrone.Events = { OpenModalDialog :'openModal', diff --git a/NzbDrone.Core/Tv/EpisodeRepository.cs b/NzbDrone.Core/Tv/EpisodeRepository.cs index 9626c35be..9b04325b3 100644 Binary files a/NzbDrone.Core/Tv/EpisodeRepository.cs and b/NzbDrone.Core/Tv/EpisodeRepository.cs differ diff --git a/NzbDrone.Core/Tvdb/TvdbEpisodes.cs b/NzbDrone.Core/Tvdb/TvdbEpisodes.cs index 2f33e7ece..79353d7f3 100644 --- a/NzbDrone.Core/Tvdb/TvdbEpisodes.cs +++ b/NzbDrone.Core/Tvdb/TvdbEpisodes.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Tvdb [XmlElement] public int EpisodeNumber { get; set; } - [XmlIgnore] + [XmlElement] public DateTime FirstAired { get; set; } [XmlElement]