From cf1e0a494617f7f9a872bd3d7f584c49e1900645 Mon Sep 17 00:00:00 2001 From: Peter Czyz Date: Mon, 3 Mar 2014 16:18:56 +0100 Subject: [PATCH 01/35] Added iCal feed for the calendar, reachable through /feed/calendar/NzbDrone.ics or through the calendar page. --- .../Calendar/CalendarFeedModule.cs | 69 +++++++++++++++++++ src/NzbDrone.Api/NzbDrone.Api.csproj | 9 ++- src/NzbDrone.Api/NzbDroneFeedModule.cs | 12 ++++ src/NzbDrone.Api/packages.config | 1 + src/UI/Calendar/CalendarFeedView.js | 16 +++++ src/UI/Calendar/CalendarFeedViewTemplate.html | 26 +++++++ src/UI/Calendar/CalendarLayout.js | 15 +++- src/UI/Calendar/CalendarLayoutTemplate.html | 9 ++- src/UI/Calendar/calendar.less | 10 +++ src/UI/index.html | 2 + 10 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 src/NzbDrone.Api/Calendar/CalendarFeedModule.cs create mode 100644 src/NzbDrone.Api/NzbDroneFeedModule.cs create mode 100644 src/UI/Calendar/CalendarFeedView.js create mode 100644 src/UI/Calendar/CalendarFeedViewTemplate.html diff --git a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs new file mode 100644 index 000000000..9bdf4ec25 --- /dev/null +++ b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs @@ -0,0 +1,69 @@ +using Nancy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DDay.iCal; +using NzbDrone.Core.Tv; +using Nancy.Responses; + +namespace NzbDrone.Api.Calendar +{ + public class CalendarFeedModule : NzbDroneFeedModule + { + private readonly IEpisodeService _episodeService; + + public CalendarFeedModule(IEpisodeService episodeService) + : base("calendar") + { + _episodeService = episodeService; + + Get["/NzbDrone.ics"] = options => GetCalendarFeed(); + } + + private Response GetCalendarFeed() + { + var start = DateTime.Today.Subtract(TimeSpan.FromDays(7)); + var end = DateTime.Today.AddDays(28); + + var queryStart = Request.Query.Start; + var queryEnd = Request.Query.End; + + if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); + if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); + + var episodes = _episodeService.EpisodesBetweenDates(start, end); + var icalCalendar = new iCalendar(); + + foreach (var series in episodes.GroupBy(v => v.Series)) + { + foreach (var episode in series) + { + var occurrence = icalCalendar.Create(); + occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString(); + occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; + occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value); + occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)); + occurrence.Description = episode.Overview; + occurrence.Categories = new List() { episode.Series.Network }; + + switch (episode.Series.SeriesType) + { + case SeriesTypes.Daily: + occurrence.Summary = string.Format("{0} - {1}", episode.Series.Title, episode.Title); + break; + + default: + occurrence.Summary = string.Format("{0} - {1}x{2:00} - {3}", episode.Series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title); + break; + } + } + } + + var serializer = new DDay.iCal.Serialization.iCalendar.SerializerFactory().Build(icalCalendar.GetType(), new DDay.iCal.Serialization.SerializationContext()) as DDay.iCal.Serialization.IStringSerializer; + var icalendar = serializer.SerializeToString(icalCalendar); + + return new TextResponse(icalendar, "text/calendar"); + } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 177d606df..ba75c4bfa 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -40,6 +40,9 @@ 4 + + ..\packages\DDay.iCal.1.0.2.575\lib\DDay.iCal.dll + False ..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll @@ -88,6 +91,7 @@ + @@ -139,6 +143,7 @@ + @@ -199,7 +204,9 @@ - + + Designer + diff --git a/src/NzbDrone.Api/NzbDroneFeedModule.cs b/src/NzbDrone.Api/NzbDroneFeedModule.cs new file mode 100644 index 000000000..d79307bef --- /dev/null +++ b/src/NzbDrone.Api/NzbDroneFeedModule.cs @@ -0,0 +1,12 @@ +using Nancy; + +namespace NzbDrone.Api +{ + public abstract class NzbDroneFeedModule : NancyModule + { + protected NzbDroneFeedModule(string resource) + : base("/feed/" + resource.Trim('/')) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/packages.config b/src/NzbDrone.Api/packages.config index bc189533c..d7ee10e3a 100644 --- a/src/NzbDrone.Api/packages.config +++ b/src/NzbDrone.Api/packages.config @@ -1,5 +1,6 @@  + diff --git a/src/UI/Calendar/CalendarFeedView.js b/src/UI/Calendar/CalendarFeedView.js new file mode 100644 index 000000000..1ceb86c01 --- /dev/null +++ b/src/UI/Calendar/CalendarFeedView.js @@ -0,0 +1,16 @@ +'use strict'; +define( + [ + 'marionette', + ], function (Marionette) { + return Marionette.Layout.extend({ + template: 'Calendar/CalendarFeedViewTemplate', + + onRender: function() { + // hackish way to determine the correct url, as using urlBase seems to only work for reverse proxies or so + var ics = '//' + window.location.host + '/feed/calendar/NzbDrone.ics'; + this.$('#ical-url').val(window.location.protocol + ics); + this.$('#ical-subscribe-button').attr('href', 'webcal:' + ics); + } + }); + }); diff --git a/src/UI/Calendar/CalendarFeedViewTemplate.html b/src/UI/Calendar/CalendarFeedViewTemplate.html new file mode 100644 index 000000000..366ec21af --- /dev/null +++ b/src/UI/Calendar/CalendarFeedViewTemplate.html @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/src/UI/Calendar/CalendarLayout.js b/src/UI/Calendar/CalendarLayout.js index 31d9e4a7f..e2772c3f5 100644 --- a/src/UI/Calendar/CalendarLayout.js +++ b/src/UI/Calendar/CalendarLayout.js @@ -1,10 +1,12 @@ 'use strict'; define( [ + 'AppLayout', 'marionette', 'Calendar/UpcomingCollectionView', - 'Calendar/CalendarView' - ], function (Marionette, UpcomingCollectionView, CalendarView) { + 'Calendar/CalendarView', + 'Calendar/CalendarFeedView' + ], function (AppLayout, Marionette, UpcomingCollectionView, CalendarView, CalendarFeedView) { return Marionette.Layout.extend({ template: 'Calendar/CalendarLayoutTemplate', @@ -12,6 +14,10 @@ define( upcoming: '#x-upcoming', calendar: '#x-calendar' }, + + events: { + 'click .x-ical': '_showiCal' + }, onShow: function () { this._showUpcoming(); @@ -24,6 +30,11 @@ define( _showCalendar: function () { this.calendar.show(new CalendarView()); + }, + + _showiCal: function () { + var view = new CalendarFeedView(); + AppLayout.modalRegion.show(view); } }); }); diff --git a/src/UI/Calendar/CalendarLayoutTemplate.html b/src/UI/Calendar/CalendarLayoutTemplate.html index 37ea4276e..a6bdc92d1 100644 --- a/src/UI/Calendar/CalendarLayoutTemplate.html +++ b/src/UI/Calendar/CalendarLayoutTemplate.html @@ -1,6 +1,13 @@ 
-

Upcoming

+
+

Upcoming

+
+
+

+ +

+
diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index 39a5e284a..8ca64ee2e 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -158,3 +158,13 @@ margin-right: 2px; } } + +.ical +{ + color: @btnInverseBackground; +} + +#ical-url +{ + width: 370px; +} \ No newline at end of file diff --git a/src/UI/index.html b/src/UI/index.html index c5ab40f60..14e06ebaf 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -23,6 +23,8 @@ + + From 794c09c17ac8b17c1f1900be86134e8a98232036 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Fri, 14 Mar 2014 21:30:49 +0100 Subject: [PATCH 02/35] New: iCal calendar feed. --- .../Calendar/CalendarFeedModule.cs | 37 +++++++++---------- src/UI/Calendar/CalendarFeedView.js | 25 +++++++++---- src/UI/Calendar/CalendarFeedViewTemplate.html | 17 +++++---- src/UI/Calendar/CalendarLayoutTemplate.html | 4 +- src/UI/Calendar/calendar.less | 12 ++++-- src/UI/index.html | 2 +- src/UI/jQuery/RouteBinder.js | 8 +++- 7 files changed, 61 insertions(+), 44 deletions(-) diff --git a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs index 9bdf4ec25..3f05eb36c 100644 --- a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Api.Calendar private Response GetCalendarFeed() { - var start = DateTime.Today.Subtract(TimeSpan.FromDays(7)); + var start = DateTime.Today.AddDays(-7); var end = DateTime.Today.AddDays(28); var queryStart = Request.Query.Start; @@ -35,28 +35,25 @@ namespace NzbDrone.Api.Calendar var episodes = _episodeService.EpisodesBetweenDates(start, end); var icalCalendar = new iCalendar(); - foreach (var series in episodes.GroupBy(v => v.Series)) + foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value)) { - foreach (var episode in series) + var occurrence = icalCalendar.Create(); + occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString(); + occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; + occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value); + occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)); + occurrence.Description = episode.Overview; + occurrence.Categories = new List() { episode.Series.Network }; + + switch (episode.Series.SeriesType) { - var occurrence = icalCalendar.Create(); - occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString(); - occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; - occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value); - occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)); - occurrence.Description = episode.Overview; - occurrence.Categories = new List() { episode.Series.Network }; + case SeriesTypes.Daily: + occurrence.Summary = string.Format("{0} - {1}", episode.Series.Title, episode.Title); + break; - switch (episode.Series.SeriesType) - { - case SeriesTypes.Daily: - occurrence.Summary = string.Format("{0} - {1}", episode.Series.Title, episode.Title); - break; - - default: - occurrence.Summary = string.Format("{0} - {1}x{2:00} - {3}", episode.Series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title); - break; - } + default: + occurrence.Summary = string.Format("{0} - {1}x{2:00} - {3}", episode.Series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title); + break; } } diff --git a/src/UI/Calendar/CalendarFeedView.js b/src/UI/Calendar/CalendarFeedView.js index 1ceb86c01..e69895af9 100644 --- a/src/UI/Calendar/CalendarFeedView.js +++ b/src/UI/Calendar/CalendarFeedView.js @@ -2,15 +2,24 @@ define( [ 'marionette', - ], function (Marionette) { + 'System/StatusModel', + 'Mixins/CopyToClipboard' + ], function (Marionette, StatusModel) { return Marionette.Layout.extend({ template: 'Calendar/CalendarFeedViewTemplate', - - onRender: function() { - // hackish way to determine the correct url, as using urlBase seems to only work for reverse proxies or so - var ics = '//' + window.location.host + '/feed/calendar/NzbDrone.ics'; - this.$('#ical-url').val(window.location.protocol + ics); - this.$('#ical-subscribe-button').attr('href', 'webcal:' + ics); - } + + ui: { + icalUrl : '.x-ical-url', + icalCopy : '.x-ical-copy' + }, + + templateHelpers: { + icalHttpUrl : window.location.protocol + '//' + window.location.host + StatusModel.get('urlBase') + '/feed/calendar/NzbDrone.ics', + icalWebCalUrl : 'webcal://' + window.location.host + StatusModel.get('urlBase') + '/feed/calendar/NzbDrone.ics' + }, + + onShow: function () { + this.ui.icalCopy.copyToClipboard(this.ui.icalUrl); + } }); }); diff --git a/src/UI/Calendar/CalendarFeedViewTemplate.html b/src/UI/Calendar/CalendarFeedViewTemplate.html index 366ec21af..4c4d8c0d9 100644 --- a/src/UI/Calendar/CalendarFeedViewTemplate.html +++ b/src/UI/Calendar/CalendarFeedViewTemplate.html @@ -7,14 +7,17 @@
- + -
- - - - - or subscribe now! +
+
+ + + +
+ + +
diff --git a/src/UI/Calendar/CalendarLayoutTemplate.html b/src/UI/Calendar/CalendarLayoutTemplate.html index a6bdc92d1..0df20bd10 100644 --- a/src/UI/Calendar/CalendarLayoutTemplate.html +++ b/src/UI/Calendar/CalendarLayoutTemplate.html @@ -5,9 +5,9 @@

- +

-
+
diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index 8ca64ee2e..430de3e0a 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -162,9 +162,13 @@ .ical { color: @btnInverseBackground; + cursor: pointer; } -#ical-url -{ - width: 370px; -} \ No newline at end of file +.ical-url { + + input { + width : 440px; + cursor : text; + } +} diff --git a/src/UI/index.html b/src/UI/index.html index 14e06ebaf..7fe301c61 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -24,7 +24,7 @@ - + diff --git a/src/UI/jQuery/RouteBinder.js b/src/UI/jQuery/RouteBinder.js index a67077a07..f4b541102 100644 --- a/src/UI/jQuery/RouteBinder.js +++ b/src/UI/jQuery/RouteBinder.js @@ -29,17 +29,21 @@ define( return; } - event.preventDefault(); - var href = event.target.getAttribute('href'); if (!href && $target.closest('a') && $target.closest('a')[0]) { var linkElement = $target.closest('a')[0]; + if ($(linkElement).hasClass('no-router')) { + return; + } + href = linkElement.getAttribute('href'); } + event.preventDefault(); + if (!href) { throw 'couldn\'t find route target'; } From b10f8a6d3fa9b982209c99a1656e311320ea575b Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 15 Mar 2014 11:13:57 +0100 Subject: [PATCH 03/35] Calendar view selection now persistent. --- src/UI/Calendar/CalendarView.js | 79 ++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js index c21e0d488..d0ed7902b 100644 --- a/src/UI/Calendar/CalendarView.js +++ b/src/UI/Calendar/CalendarView.js @@ -8,25 +8,24 @@ define( 'Calendar/Collection', 'System/StatusModel', 'History/Queue/QueueCollection', + 'Config', 'Mixins/backbone.signalr.mixin', 'fullcalendar', 'jquery.easypiechart' - ], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection) { - - var _instance; + ], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection, Config) { return Marionette.ItemView.extend({ + storageKey: 'calendar.view', + initialize: function () { this.collection = new CalendarCollection().bindSignalR({ updateOnly: true }); this.listenTo(this.collection, 'change', this._reloadCalendarEvents); this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents); }, + render : function () { - - var self = this; - this.$el.empty().fullCalendar({ - defaultView : 'basicWeek', + defaultView : Config.getValue(this.storageKey, 'basicWeek'), allDayDefault : false, ignoreTimezone: false, weekMode : 'variable', @@ -41,54 +40,62 @@ define( prev: '', next: '' }, - viewRender : this._getEvents, - eventRender : function (event, element) { - self.$(element).addClass(event.statusLevel); - self.$(element).children('.fc-event-inner').addClass(event.statusLevel); - - if (event.progress > 0) { - self.$(element).find('.fc-event-time') - .after(''.format(event.progress)); - - self.$(element).find('.chart').easyPieChart({ - barColor : '#ffffff', - trackColor: false, - scaleColor: false, - lineWidth : 2, - size : 14, - animate : false - }); - } - }, + viewRender : this._viewRender.bind(this), + eventRender : this._eventRender.bind(this), eventClick : function (event) { vent.trigger(vent.Commands.ShowEpisodeDetails, {episode: event.model}); } }); - - _instance = this; }, onShow: function () { this.$('.fc-button-today').click(); }, + _viewRender: function (view) { + if (Config.getValue(this.storageKey) !== view.name) { + Config.setValue(this.storageKey, view.name); + } + + this._getEvents(view); + }, + + _eventRender: function (event, element) { + this.$(element).addClass(event.statusLevel); + this.$(element).children('.fc-event-inner').addClass(event.statusLevel); + + if (event.progress > 0) { + this.$(element).find('.fc-event-time') + .after(''.format(event.progress)); + + this.$(element).find('.chart').easyPieChart({ + barColor : '#ffffff', + trackColor: false, + scaleColor: false, + lineWidth : 2, + size : 14, + animate : false + }); + } + }, + _getEvents: function (view) { var start = moment(view.visStart).toISOString(); var end = moment(view.visEnd).toISOString(); - _instance.$el.fullCalendar('removeEvents'); + this.$el.fullCalendar('removeEvents'); - _instance.collection.fetch({ + this.collection.fetch({ data : { start: start, end: end }, - success: function (collection) { - _instance._setEventData(collection); - } + success: this._setEventData.bind(this) }); }, _setEventData: function (collection) { var events = []; + var self = this; + collection.each(function (model) { var seriesTitle = model.get('series').title; var start = model.get('airDateUtc'); @@ -100,15 +107,15 @@ define( start : start, end : end, allDay : false, - statusLevel : _instance._getStatusLevel(model, end), - progress : _instance._getDownloadProgress(model), + statusLevel : self._getStatusLevel(model, end), + progress : self._getDownloadProgress(model), model : model }; events.push(event); }); - _instance.$el.fullCalendar('addEventSource', events); + this.$el.fullCalendar('addEventSource', events); }, _getStatusLevel: function (element, endTime) { From 58fa56968c97a543774606da5655a93c13a1a8e4 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 17 Mar 2014 13:41:13 -0700 Subject: [PATCH 04/35] Added DLL map for media info on solaris --- src/MediaInfoDotNet.dll.config | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MediaInfoDotNet.dll.config b/src/MediaInfoDotNet.dll.config index 4fe730868..bd2707d4b 100644 --- a/src/MediaInfoDotNet.dll.config +++ b/src/MediaInfoDotNet.dll.config @@ -3,4 +3,5 @@ + From e415c7aaa25f3c3f89f721b0ccdc02d88aab28f4 Mon Sep 17 00:00:00 2001 From: Andrew Chappell Date: Wed, 12 Mar 2014 20:23:16 +0000 Subject: [PATCH 05/35] Added support for WDTV metadata. Correctly writes out xml files for episode metadata, .metathumb files (jpegs) for episode stills and folder.jpgs for series / season images. --- .../MetaData/Consumers/Wdtv/WdtvMetadata.cs | 475 ++++++++++++++++++ .../Consumers/Wdtv/WdtvMetadataSettings.cs | 53 ++ src/NzbDrone.Core/NzbDrone.Core.csproj | 2 + 3 files changed, 530 insertions(+) create mode 100644 src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs create mode 100644 src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadataSettings.cs diff --git a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs new file mode 100644 index 000000000..e76a341bc --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs @@ -0,0 +1,475 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Remoting.Messaging; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Metadata.Files; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata.Consumers.Wdtv +{ + public class WdtvMetadata : MetadataBase + { + private readonly IEventAggregator _eventAggregator; + private readonly IMapCoversToLocal _mediaCoverService; + private readonly IMediaFileService _mediaFileService; + private readonly IMetadataFileService _metadataFileService; + private readonly IDiskProvider _diskProvider; + private readonly IHttpProvider _httpProvider; + private readonly IEpisodeService _episodeService; + private readonly Logger _logger; + + public WdtvMetadata(IEventAggregator eventAggregator, + IMapCoversToLocal mediaCoverService, + IMediaFileService mediaFileService, + IMetadataFileService metadataFileService, + IDiskProvider diskProvider, + IHttpProvider httpProvider, + IEpisodeService episodeService, + Logger logger) + : base(diskProvider, httpProvider, logger) + { + _eventAggregator = eventAggregator; + _mediaCoverService = mediaCoverService; + _mediaFileService = mediaFileService; + _metadataFileService = metadataFileService; + _diskProvider = diskProvider; + _httpProvider = httpProvider; + _episodeService = episodeService; + _logger = logger; + } + + private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public override void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles) + { + var metadataFiles = new List(); + + if (!_diskProvider.FolderExists(series.Path)) + { + _logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path); + return; + } + + if (Settings.SeriesImages) + { + var metadata = WriteSeriesImages(series, existingMetadataFiles); + if (metadata != null) + { + metadataFiles.Add(metadata); + } + } + + if (Settings.SeasonImages) + { + var metadata = WriteSeasonImages(series, existingMetadataFiles); + if (metadata != null) + { + metadataFiles.AddRange(metadata); + } + } + + foreach (var episodeFile in episodeFiles) + { + if (Settings.EpisodeMetadata) + { + var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles); + if (metadata != null) + { + metadataFiles.Add(metadata); + } + } + } + + foreach (var episodeFile in episodeFiles) + { + if (Settings.EpisodeImages) + { + var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles); + + if (metadataFile != null) + { + metadataFiles.Add(metadataFile); + } + } + } + metadataFiles.RemoveAll(c => c == null); + _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); + } + + public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) + { + var metadataFiles = new List(); + + if (Settings.EpisodeMetadata) + { + metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List())); + } + + if (Settings.EpisodeImages) + { + var metadataFile = WriteEpisodeImages(series, episodeFile, new List()); + + if (metadataFile != null) + { + metadataFiles.Add(metadataFile); + } + } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); + } + + public override void AfterRename(Series series, List existingMetadataFiles, List episodeFiles) + { + var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); + var updatedMetadataFiles = new List(); + + foreach (var episodeFile in episodeFiles) + { + var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); + + foreach (var metadataFile in metadataFiles) + { + string newFilename; + + if (metadataFile.Type == MetadataType.EpisodeImage) + { + newFilename = GetEpisodeImageFilename(episodeFile.Path); + } + + else if (metadataFile.Type == MetadataType.EpisodeMetadata) + { + newFilename = GetEpisodeMetadataFilename(episodeFile.Path); + } + + else + { + _logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath); + continue; + } + + var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); + + if (!newFilename.PathEquals(existingFilename)) + { + _diskProvider.MoveFile(existingFilename, newFilename); + metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + + updatedMetadataFiles.Add(metadataFile); + } + } + } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); + } + + public override MetadataFile FindMetadataFile(Series series, string path) + { + var filename = Path.GetFileName(path); + + if (filename == null) return null; + + var metadata = new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + }; + + //Series and season images are both named folder.jpg, only season ones sit in season folders + if (String.Compare(filename, "folder.jpg", true) == 0) + { + var parentdir = Directory.GetParent(path); + var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); + if (seasonMatch.Success) + { + metadata.Type = MetadataType.SeasonImage; + + var seasonNumber = seasonMatch.Groups["season"].Value; + + if (seasonNumber.Contains("specials")) + { + metadata.SeasonNumber = 0; + } + else + { + metadata.SeasonNumber = Convert.ToInt32(seasonNumber); + } + + return metadata; + } + else + { + metadata.Type = MetadataType.SeriesImage; + return metadata; + } + } + + var parseResult = Parser.Parser.ParseTitle(filename); + + if (parseResult != null && + !parseResult.FullSeason) + { + switch (Path.GetExtension(filename).ToLowerInvariant()) + { + case ".xml": + metadata.Type = MetadataType.EpisodeMetadata; + return metadata; + case ".metathumb": + metadata.Type = MetadataType.EpisodeImage; + return metadata; + } + + } + + return null; + } + + private MetadataFile WriteSeriesImages(Series series, List existingMetadataFiles) + { + //Because we only support one image, attempt to get the Poster type, then if that fails grab the first + var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); + if (image == null) + { + _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); + return null; + } + + var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var destination = Path.Combine(series.Path, "folder" + Path.GetExtension(source)); + + //TODO: Do we want to overwrite the file if it exists? + if (_diskProvider.FileExists(destination)) + { + _logger.Debug("Series image: {0} already exists.", image.CoverType); + return null; + } + else + { + + _diskProvider.CopyFile(source, destination, false); + + var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ?? + new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + Type = MetadataType.SeriesImage, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination) + }; + + return metadata; + } + } + + private IEnumerable WriteSeasonImages(Series series, List existingMetadataFiles) + { + _logger.Debug("Writing season images for {0}.", series.Title); + //Create a dictionary between season number and output folder + var seasonFolderMap = new Dictionary(); + foreach (var folder in Directory.EnumerateDirectories(series.Path)) + { + var directoryinfo = new DirectoryInfo(folder); + var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); + if (seasonMatch.Success) + { + var seasonNumber = seasonMatch.Groups["season"].Value; + + if (seasonNumber.Contains("specials")) + { + seasonFolderMap[0] = folder; + } + else + { + int matchedSeason; + if (Int32.TryParse(seasonNumber, out matchedSeason)) + { + seasonFolderMap[matchedSeason] = folder; + } + else + { + _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title); + } + } + } + else + { + _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); + } + } + foreach (var season in series.Seasons) + { + //Work out the path to this season - if we don't have a matching path then skip this season. + string seasonFolder; + if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder)) + { + _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); + continue; + } + + //WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection + var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); + if (image == null) + { + _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); + continue; + } + + + var filename = "folder.jpg"; + + var path = Path.Combine(series.Path, seasonFolder, filename); + _logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path); + DownloadImage(series, image.Url, path); + + var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && + c.SeasonNumber == season.SeasonNumber) ?? + new MetadataFile + { + SeriesId = series.Id, + SeasonNumber = season.SeasonNumber, + Consumer = GetType().Name, + Type = MetadataType.SeasonImage, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + }; + + yield return metadata; + } + } + + private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List existingMetadataFiles) + { + var filename = GetEpisodeMetadataFilename(episodeFile.Path); + var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); + + var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && + c.EpisodeFileId == episodeFile.Id); + + if (existingMetadata != null) + { + var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + if (!filename.PathEquals(fullPath)) + { + _diskProvider.MoveFile(fullPath, filename); + existingMetadata.RelativePath = relativePath; + } + } + + _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path); + + var xmlResult = String.Empty; + foreach (var episode in episodeFile.Episodes.Value) + { + var sb = new StringBuilder(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = true; + xws.Indent = false; + + using (var xw = XmlWriter.Create(sb, xws)) + { + var doc = new XDocument(); + + var details = new XElement("details"); + details.Add(new XElement("id", series.Id)); + details.Add(new XElement("title", String.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); + details.Add(new XElement("series_name", series.Title)); + details.Add(new XElement("episode_name", episode.Title)); + details.Add(new XElement("season_number", episode.SeasonNumber)); + details.Add(new XElement("episode_number", episode.EpisodeNumber)); + details.Add(new XElement("firstaired", episode.AirDate)); + details.Add(new XElement("genre", String.Join(" / ", series.Genres))); + details.Add(new XElement("actor", String.Join(" / ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character)))); + details.Add(new XElement("overview", episode.Overview)); + + + //Todo: get guest stars, writer and director + //details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault())); + //details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault())); + + doc.Add(details); + doc.Save(xw); + + xmlResult += doc.ToString(); + xmlResult += Environment.NewLine; + } + } + + _logger.Debug("Saving episodedetails to: {0}", filename); + _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); + + var metadata = existingMetadata ?? + new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = GetType().Name, + Type = MetadataType.EpisodeMetadata, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) + }; + + return metadata; + } + + private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List existingMetadataFiles) + { + var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + + if (screenshot == null) + { + _logger.Trace("Episode screenshot not available"); + return null; + } + + var filename = GetEpisodeImageFilename(episodeFile.Path); + var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); + + var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage && + c.EpisodeFileId == episodeFile.Id); + + if (existingMetadata != null) + { + var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + if (!filename.PathEquals(fullPath)) + { + _diskProvider.MoveFile(fullPath, filename); + existingMetadata.RelativePath = relativePath; + } + } + + DownloadImage(series, screenshot.Url, filename); + + var metadata = existingMetadata ?? + new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = GetType().Name, + Type = MetadataType.EpisodeImage, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) + }; + + return metadata; + } + + private string GetEpisodeMetadataFilename(string episodeFilePath) + { + return Path.ChangeExtension(episodeFilePath, "xml"); + } + + private string GetEpisodeImageFilename(string episodeFilePath) + { + return Path.ChangeExtension(episodeFilePath, "metathumb"); + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadataSettings.cs b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadataSettings.cs new file mode 100644 index 000000000..b10b4247c --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadataSettings.cs @@ -0,0 +1,53 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Metadata.Consumers.Wdtv +{ + public class WdtvSettingsValidator : AbstractValidator + { + public WdtvSettingsValidator() + { + } + } + + public class WdtvMetadataSettings : IProviderConfig + { + private static readonly WdtvSettingsValidator Validator = new WdtvSettingsValidator(); + + public WdtvMetadataSettings() + { + EpisodeMetadata = true; + SeriesImages = true; + SeasonImages = true; + EpisodeImages = true; + } + + [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] + public Boolean EpisodeMetadata { get; set; } + + [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] + public Boolean SeriesImages { get; set; } + + [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)] + public Boolean SeasonImages { get; set; } + + [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)] + public Boolean EpisodeImages { get; set; } + + public bool IsValid + { + get + { + return true; + } + } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 640fef368..d608cc7cc 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -334,6 +334,8 @@ + + From b60633882e1d7862baa952c464b2c481179c0fd0 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 17 Mar 2014 17:18:03 -0700 Subject: [PATCH 06/35] Fixed: Only vacuum the main db on startup --- src/NzbDrone.Core/Datastore/DbFactory.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/DbFactory.cs b/src/NzbDrone.Core/Datastore/DbFactory.cs index 6fea8181a..6d0e4a615 100644 --- a/src/NzbDrone.Core/Datastore/DbFactory.cs +++ b/src/NzbDrone.Core/Datastore/DbFactory.cs @@ -15,7 +15,6 @@ namespace NzbDrone.Core.Datastore IDatabase Create(MigrationType migrationType = MigrationType.Main); } - public class DbFactory : IDbFactory { private readonly IMigrationController _migrationController; @@ -79,8 +78,11 @@ namespace NzbDrone.Core.Datastore return dataMapper; }); - db.Vacuum(); + if (migrationType == MigrationType.Main) + { + db.Vacuum(); + } return db; } From a868a0742a36be0df11be3e82847aecb2d235c61 Mon Sep 17 00:00:00 2001 From: Andrew Chappell Date: Wed, 19 Mar 2014 23:12:58 +0000 Subject: [PATCH 07/35] Support for Roksbox Metadata. Outputs Series, Season and Episode images along with xml metadata. --- .../Consumers/Roksbox/RoksboxMetadata.cs | 468 ++++++++++++++++++ .../Roksbox/RoksboxMetadataSettings.cs | 53 ++ src/NzbDrone.Core/NzbDrone.Core.csproj | 2 + 3 files changed, 523 insertions(+) create mode 100644 src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs create mode 100644 src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadataSettings.cs diff --git a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs new file mode 100644 index 000000000..b6b396d69 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs @@ -0,0 +1,468 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Remoting.Messaging; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Metadata.Files; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Metadata.Consumers.Roksbox +{ + public class RoksboxMetadata : MetadataBase + { + private readonly IEventAggregator _eventAggregator; + private readonly IMapCoversToLocal _mediaCoverService; + private readonly IMediaFileService _mediaFileService; + private readonly IMetadataFileService _metadataFileService; + private readonly IDiskProvider _diskProvider; + private readonly IHttpProvider _httpProvider; + private readonly IEpisodeService _episodeService; + private readonly Logger _logger; + + public RoksboxMetadata(IEventAggregator eventAggregator, + IMapCoversToLocal mediaCoverService, + IMediaFileService mediaFileService, + IMetadataFileService metadataFileService, + IDiskProvider diskProvider, + IHttpProvider httpProvider, + IEpisodeService episodeService, + Logger logger) + : base(diskProvider, httpProvider, logger) + { + _eventAggregator = eventAggregator; + _mediaCoverService = mediaCoverService; + _mediaFileService = mediaFileService; + _metadataFileService = metadataFileService; + _diskProvider = diskProvider; + _httpProvider = httpProvider; + _episodeService = episodeService; + _logger = logger; + } + + private static List ValidCertification = new List { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" }; + private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public override void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles) + { + var metadataFiles = new List(); + + if (!_diskProvider.FolderExists(series.Path)) + { + _logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path); + return; + } + + if (Settings.SeriesImages) + { + var metadata = WriteSeriesImages(series, existingMetadataFiles); + if (metadata != null) + { + metadataFiles.Add(metadata); + } + } + + if (Settings.SeasonImages) + { + var metadata = WriteSeasonImages(series, existingMetadataFiles); + if (metadata != null) + { + metadataFiles.AddRange(metadata); + } + } + + foreach (var episodeFile in episodeFiles) + { + if (Settings.EpisodeMetadata) + { + var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles); + if (metadata != null) + { + metadataFiles.Add(metadata); + } + } + } + + foreach (var episodeFile in episodeFiles) + { + if (Settings.EpisodeImages) + { + var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles); + + if (metadataFile != null) + { + metadataFiles.Add(metadataFile); + } + } + } + metadataFiles.RemoveAll(c => c == null); + _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); + } + + public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) + { + var metadataFiles = new List(); + + if (Settings.EpisodeMetadata) + { + metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List())); + } + + if (Settings.EpisodeImages) + { + var metadataFile = WriteEpisodeImages(series, episodeFile, new List()); + + if (metadataFile != null) + { + metadataFiles.Add(metadataFile); + } + } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); + } + + public override void AfterRename(Series series, List existingMetadataFiles, List episodeFiles) + { + var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); + var updatedMetadataFiles = new List(); + + foreach (var episodeFile in episodeFiles) + { + var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); + + foreach (var metadataFile in metadataFiles) + { + string newFilename; + + if (metadataFile.Type == MetadataType.EpisodeImage) + { + newFilename = GetEpisodeImageFilename(episodeFile.Path); + } + + else if (metadataFile.Type == MetadataType.EpisodeMetadata) + { + newFilename = GetEpisodeMetadataFilename(episodeFile.Path); + } + + else + { + _logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath); + continue; + } + + var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); + + if (!newFilename.PathEquals(existingFilename)) + { + _diskProvider.MoveFile(existingFilename, newFilename); + metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + + updatedMetadataFiles.Add(metadataFile); + } + } + } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); + } + + public override MetadataFile FindMetadataFile(Series series, string path) + { + var filename = Path.GetFileName(path); + + if (filename == null) return null; + var parentdir = Directory.GetParent(path); + + var metadata = new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + }; + + //Series and season images are both named folder.jpg, only season ones sit in season folders + if (String.Compare(filename, parentdir.Name, true) == 0) + { + var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); + if (seasonMatch.Success) + { + metadata.Type = MetadataType.SeasonImage; + + var seasonNumber = seasonMatch.Groups["season"].Value; + + if (seasonNumber.Contains("specials")) + { + metadata.SeasonNumber = 0; + } + else + { + metadata.SeasonNumber = Convert.ToInt32(seasonNumber); + } + + return metadata; + } + else + { + metadata.Type = MetadataType.SeriesImage; + return metadata; + } + } + + var parseResult = Parser.Parser.ParseTitle(filename); + + if (parseResult != null && + !parseResult.FullSeason) + { + switch (Path.GetExtension(filename).ToLowerInvariant()) + { + case ".xml": + metadata.Type = MetadataType.EpisodeMetadata; + return metadata; + case ".jpg": + metadata.Type = MetadataType.EpisodeImage; + return metadata; + } + + } + + return null; + } + + private MetadataFile WriteSeriesImages(Series series, List existingMetadataFiles) + { + //Because we only support one image, attempt to get the Poster type, then if that fails grab the first + var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); + if (image == null) + { + _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); + return null; + } + + var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source)); + + //TODO: Do we want to overwrite the file if it exists? + if (_diskProvider.FileExists(destination)) + { + _logger.Debug("Series image: {0} already exists.", image.CoverType); + return null; + } + else + { + + _diskProvider.CopyFile(source, destination, false); + + var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ?? + new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + Type = MetadataType.SeriesImage, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination) + }; + + return metadata; + } + } + + private IEnumerable WriteSeasonImages(Series series, List existingMetadataFiles) + { + _logger.Debug("Writing season images for {0}.", series.Title); + //Create a dictionary between season number and output folder + var seasonFolderMap = new Dictionary(); + foreach (var folder in Directory.EnumerateDirectories(series.Path)) + { + var directoryinfo = new DirectoryInfo(folder); + var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); + if (seasonMatch.Success) + { + var seasonNumber = seasonMatch.Groups["season"].Value; + + if (seasonNumber.Contains("specials")) + { + seasonFolderMap[0] = folder; + } + else + { + int matchedSeason; + if (Int32.TryParse(seasonNumber, out matchedSeason)) + { + seasonFolderMap[matchedSeason] = folder; + } + else + { + _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title); + } + } + } + else + { + _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); + } + } + foreach (var season in series.Seasons) + { + //Work out the path to this season - if we don't have a matching path then skip this season. + string seasonFolder; + if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder)) + { + _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); + continue; + } + + //Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection + var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); + if (image == null) + { + _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); + continue; + } + + + var filename = Path.GetFileName(seasonFolder) + ".jpg"; + + var path = Path.Combine(series.Path, seasonFolder, filename); + _logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path); + DownloadImage(series, image.Url, path); + + var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && + c.SeasonNumber == season.SeasonNumber) ?? + new MetadataFile + { + SeriesId = series.Id, + SeasonNumber = season.SeasonNumber, + Consumer = GetType().Name, + Type = MetadataType.SeasonImage, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + }; + + yield return metadata; + } + } + + private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List existingMetadataFiles) + { + var filename = GetEpisodeMetadataFilename(episodeFile.Path); + var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); + + var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && + c.EpisodeFileId == episodeFile.Id); + + if (existingMetadata != null) + { + var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + if (!filename.PathEquals(fullPath)) + { + _diskProvider.MoveFile(fullPath, filename); + existingMetadata.RelativePath = relativePath; + } + } + + _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path); + + var xmlResult = String.Empty; + foreach (var episode in episodeFile.Episodes.Value) + { + var sb = new StringBuilder(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = true; + xws.Indent = false; + + using (var xw = XmlWriter.Create(sb, xws)) + { + var doc = new XDocument(); + + var details = new XElement("video"); + details.Add(new XElement("title", String.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); + details.Add(new XElement("year", episode.AirDate)); + details.Add(new XElement("genre", String.Join(" / ", series.Genres))); + var actors = String.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count))); + details.Add(new XElement("actors", actors)); + details.Add(new XElement("description", episode.Overview)); + details.Add(new XElement("length", series.Runtime)); + details.Add(new XElement("mpaa", ValidCertification.Contains( series.Certification.ToUpperInvariant() ) ? series.Certification.ToUpperInvariant() : "UNRATED" ) ); + doc.Add(details); + doc.Save(xw); + + xmlResult += doc.ToString(); + xmlResult += Environment.NewLine; + } + } + + _logger.Debug("Saving episodedetails to: {0}", filename); + _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); + + var metadata = existingMetadata ?? + new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = GetType().Name, + Type = MetadataType.EpisodeMetadata, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) + }; + + return metadata; + } + + private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List existingMetadataFiles) + { + var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + + if (screenshot == null) + { + _logger.Trace("Episode screenshot not available"); + return null; + } + + var filename = GetEpisodeImageFilename(episodeFile.Path); + var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); + + var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage && + c.EpisodeFileId == episodeFile.Id); + + if (existingMetadata != null) + { + var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + if (!filename.PathEquals(fullPath)) + { + _diskProvider.MoveFile(fullPath, filename); + existingMetadata.RelativePath = relativePath; + } + } + + DownloadImage(series, screenshot.Url, filename); + + var metadata = existingMetadata ?? + new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = GetType().Name, + Type = MetadataType.EpisodeImage, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) + }; + + return metadata; + } + + private string GetEpisodeMetadataFilename(string episodeFilePath) + { + return Path.ChangeExtension(episodeFilePath, "xml"); + } + + private string GetEpisodeImageFilename(string episodeFilePath) + { + return Path.ChangeExtension(episodeFilePath, "jpg"); + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadataSettings.cs b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadataSettings.cs new file mode 100644 index 000000000..81b410e4a --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadataSettings.cs @@ -0,0 +1,53 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Metadata.Consumers.Roksbox +{ + public class RoksboxSettingsValidator : AbstractValidator + { + public RoksboxSettingsValidator() + { + } + } + + public class RoksboxMetadataSettings : IProviderConfig + { + private static readonly RoksboxSettingsValidator Validator = new RoksboxSettingsValidator(); + + public RoksboxMetadataSettings() + { + EpisodeMetadata = true; + SeriesImages = true; + SeasonImages = true; + EpisodeImages = true; + } + + [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] + public Boolean EpisodeMetadata { get; set; } + + [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] + public Boolean SeriesImages { get; set; } + + [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)] + public Boolean SeasonImages { get; set; } + + [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)] + public Boolean EpisodeImages { get; set; } + + public bool IsValid + { + get + { + return true; + } + } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 3f75a0767..03ff7415d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -341,6 +341,8 @@ + + From bac75ac6d965311d477091380091293dd16f2b61 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 20 Mar 2014 00:08:15 -0700 Subject: [PATCH 08/35] New: Failed download handling for Nzbget --- .../Extensions/StreamExtensions.cs | 21 ++++++ src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + .../ParserTests/CrapParserFixture.cs | 1 + .../ParserTests/PathParserFixture.cs | 1 + .../Clients/Nzbget/NzbGetQueueItem.cs | 4 + .../Download/Clients/Nzbget/Nzbget.cs | 70 ++++++++++++++---- ...ueResponse.cs => NzbgetBooleanResponse.cs} | 2 +- .../Clients/Nzbget/NzbgetHistoryItem.cs | 18 +++++ .../{NzbGetQueue.cs => NzbgetListResponse.cs} | 4 +- .../Clients/Nzbget/NzbgetParameter.cs | 9 +++ .../Download/Clients/Nzbget/NzbgetProxy.cs | 73 +++++++++++++++++-- .../Download/Clients/Sabnzbd/SabnzbdProxy.cs | 18 +---- .../Download/DownloadClientBase.cs | 1 - src/NzbDrone.Core/NzbDrone.Core.csproj | 6 +- 14 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 src/NzbDrone.Common/Extensions/StreamExtensions.cs rename src/NzbDrone.Core/Download/Clients/Nzbget/{EnqueueResponse.cs => NzbgetBooleanResponse.cs} (81%) create mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs rename src/NzbDrone.Core/Download/Clients/Nzbget/{NzbGetQueue.cs => NzbgetListResponse.cs} (71%) create mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetParameter.cs diff --git a/src/NzbDrone.Common/Extensions/StreamExtensions.cs b/src/NzbDrone.Common/Extensions/StreamExtensions.cs new file mode 100644 index 000000000..6283f5fc0 --- /dev/null +++ b/src/NzbDrone.Common/Extensions/StreamExtensions.cs @@ -0,0 +1,21 @@ +using System.IO; + +namespace NzbDrone.Common.Extensions +{ + public static class StreamExtensions + { + public static byte[] ToBytes(this Stream input) + { + var buffer = new byte[16 * 1024]; + using (var ms = new MemoryStream()) + { + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } + } + } +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 6ee9aeeb4..9d1e80e38 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -107,6 +107,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs index 83fd99d00..9ed19f36f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs @@ -27,6 +27,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("86420f8ee425340d8894bf3bc636b66404b95f18")] [TestCase("ce39afb7da6cf7c04eba3090f0a309f609883862")] [TestCase("THIS SHOULD NEVER PARSE")] + [TestCase("Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv")] public void should_not_parse_crap(string title) { Parser.Parser.ParseTitle(title).Should().BeNull(); diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs index d9f46806a..2eed10534 100644 --- a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -31,6 +31,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase(@"C:\Test\Unsorted\The.Big.Bang.Theory.S01E01.720p.HDTV\tbbt101.avi", 1, 1)] [TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E19.720p.BluRay.x264-SiNNERS-RP\ba27283b17c00d01193eacc02a8ba98eeb523a76.mkv", 2, 19)] [TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E18.720p.BluRay.x264-SiNNERS-RP\45a55debe3856da318cc35882ad07e43cd32fd15.mkv", 2, 18)] + [TestCase(@"C:\Test\The.Blacklist.S01E16.720p.HDTV.X264-DIMENSION\XRmZciqkBopq4851Ddbipe\Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv", 1, 16)] public void should_parse_from_path(string path, int season, int episode) { var result = Parser.Parser.ParsePath(path); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs index 39bc8eb51..38292bb26 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.Nzbget { @@ -6,10 +7,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { private string _nzbName; public Int32 NzbId { get; set; } + public Int32 FirstId { get; set; } + public Int32 LastId { get; set; } public string NzbName { get; set; } public String Category { get; set; } public Int32 FileSizeMb { get; set; } public Int32 RemainingSizeMb { get; set; } public Int32 PausedSizeMb { get; set; } + public List Parameters { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 07221549d..46d4cd715 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -13,14 +14,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { private readonly INzbgetProxy _proxy; private readonly IParsingService _parsingService; + private readonly IHttpProvider _httpProvider; private readonly Logger _logger; public Nzbget(INzbgetProxy proxy, IParsingService parsingService, + IHttpProvider httpProvider, Logger logger) { _proxy = proxy; _parsingService = parsingService; + _httpProvider = httpProvider; _logger = logger; } @@ -29,16 +33,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title + ".nzb"; - string cat = Settings.TvCategory; + string category = Settings.TvCategory; int priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; _logger.Info("Adding report [{0}] to the queue.", title); - var success = _proxy.AddNzb(Settings, title, cat, priority, false, url); + using (var nzb = _httpProvider.DownloadStream(url)) + { + _logger.Info("Adding report [{0}] to the queue.", title); + var response = _proxy.DownloadNzb(nzb, title, category, priority, Settings); - _logger.Debug("Queue Response: [{0}]", success); - - return null; + return response; + } } public override IEnumerable GetQueue() @@ -57,14 +63,16 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var queueItems = new List(); - foreach (var nzbGetQueueItem in queue) + foreach (var item in queue) { + var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); + var queueItem = new QueueItem(); - queueItem.Id = nzbGetQueueItem.NzbId.ToString(); - queueItem.Title = nzbGetQueueItem.NzbName; - queueItem.Size = nzbGetQueueItem.FileSizeMb; - queueItem.Sizeleft = nzbGetQueueItem.RemainingSizeMb; - queueItem.Status = nzbGetQueueItem.FileSizeMb == nzbGetQueueItem.PausedSizeMb ? "paused" : "queued"; + queueItem.Id = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(); + queueItem.Title = item.NzbName; + queueItem.Size = item.FileSizeMb; + queueItem.Sizeleft = item.RemainingSizeMb; + queueItem.Status = item.FileSizeMb == item.PausedSizeMb ? "paused" : "queued"; var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title); if (parsedEpisodeInfo == null) continue; @@ -81,7 +89,43 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public override IEnumerable GetHistory(int start = 0, int limit = 10) { - return new HistoryItem[0]; + List history; + + try + { + history = _proxy.GetHistory(Settings); + } + catch (DownloadClientException ex) + { + _logger.ErrorException(ex.Message, ex); + return Enumerable.Empty(); + } + + var historyItems = new List(); + var successStatues = new[] {"SUCCESS", "NONE"}; + + foreach (var item in history) + { + var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); + var status = successStatues.Contains(item.ParStatus) && + successStatues.Contains(item.ScriptStatus) + ? HistoryStatus.Completed + : HistoryStatus.Failed; + + var historyItem = new HistoryItem(); + historyItem.Id = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); + historyItem.Title = item.Name; + historyItem.Size = item.FileSizeMb.ToString(); //Why is this a string? + historyItem.DownloadTime = 0; + historyItem.Storage = item.DestDir; + historyItem.Category = item.Category; + historyItem.Message = String.Format("PAR Status: {0} - Script Status: {1}", item.ParStatus, item.ScriptStatus); + historyItem.Status = status; + + historyItems.Add(historyItem); + } + + return historyItems; } public override void RemoveFromQueue(string id) @@ -91,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public override void RemoveFromHistory(string id) { - throw new NotImplementedException(); + _proxy.RemoveFromHistory(id, Settings); } public override void Test() diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/EnqueueResponse.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs similarity index 81% rename from src/NzbDrone.Core/Download/Clients/Nzbget/EnqueueResponse.cs rename to src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs index f16799151..6c536ba7d 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/EnqueueResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs @@ -2,7 +2,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { - public class EnqueueResponse + public class NzbgetBooleanResponse { public String Version { get; set; } public Boolean Result { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs new file mode 100644 index 000000000..af90178a8 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetHistoryItem + { + private string _nzbName; + public Int32 Id { get; set; } + public String Name { get; set; } + public String Category { get; set; } + public Int32 FileSizeMb { get; set; } + public String ParStatus { get; set; } + public String ScriptStatus { get; set; } + public String DestDir { get; set; } + public List Parameters { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueue.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs similarity index 71% rename from src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueue.cs rename to src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs index f7ec8a1be..bb51dbcc6 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueue.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs @@ -4,11 +4,11 @@ using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Nzbget { - public class NzbgetQueue + public class NzbgetListResponse { public String Version { get; set; } [JsonProperty(PropertyName = "result")] - public List QueueItems { get; set; } + public List QueueItems { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetParameter.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetParameter.cs new file mode 100644 index 000000000..d2b728e85 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetParameter.cs @@ -0,0 +1,9 @@ +using System; +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetParameter + { + public String Name { get; set; } + public object Value { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index 678a63a56..91d4de26d 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Rest; using RestSharp; @@ -9,9 +12,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { public interface INzbgetProxy { - bool AddNzb(NzbgetSettings settings, params object[] parameters); + string DownloadNzb(Stream nzb, string title, string category, int priority, NzbgetSettings settings); List GetQueue(NzbgetSettings settings); + List GetHistory(NzbgetSettings settings); VersionResponse GetVersion(NzbgetSettings settings); + void RemoveFromHistory(string id, NzbgetSettings settings); } public class NzbgetProxy : INzbgetProxy @@ -23,18 +28,50 @@ namespace NzbDrone.Core.Download.Clients.Nzbget _logger = logger; } - public bool AddNzb(NzbgetSettings settings, params object[] parameters) + public string DownloadNzb(Stream nzb, string title, string category, int priority, NzbgetSettings settings) { - var request = BuildRequest(new JsonRequest("appendurl", parameters)); + var parameters = new object[] { title, category, priority, false, Convert.ToBase64String(nzb.ToBytes()) }; + var request = BuildRequest(new JsonRequest("append", parameters)); - return Json.Deserialize(ProcessRequest(request, settings)).Result; + var response = Json.Deserialize(ProcessRequest(request, settings)); + _logger.Debug("Queue Response: [{0}]", response.Result); + + if (!response.Result) + { + return null; + } + + var queue = GetQueue(settings); + var item = queue.FirstOrDefault(q => q.NzbName == title.Substring(0, title.Length - 4)); + + if (item == null) + { + return null; + } + + var droneId = Guid.NewGuid().ToString().Replace("-", ""); + var editResult = EditQueue("GroupSetParameter", 0, "drone=" + droneId, item.LastId, settings); + + if (editResult) + { + _logger.Debug("Nzbget download drone parameter set to: {0}", droneId); + } + + return droneId; } public List GetQueue(NzbgetSettings settings) { var request = BuildRequest(new JsonRequest("listgroups")); - return Json.Deserialize(ProcessRequest(request, settings)).QueueItems; + return Json.Deserialize>(ProcessRequest(request, settings)).QueueItems; + } + + public List GetHistory(NzbgetSettings settings) + { + var request = BuildRequest(new JsonRequest("history")); + + return Json.Deserialize>(ProcessRequest(request, settings)).QueueItems; } public VersionResponse GetVersion(NzbgetSettings settings) @@ -44,6 +81,32 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return Json.Deserialize(ProcessRequest(request, settings)); } + public void RemoveFromHistory(string id, NzbgetSettings settings) + { + var history = GetHistory(settings); + var item = history.SingleOrDefault(h => h.Parameters.SingleOrDefault(p => p.Name == "drone") != null); + + if (item == null) + { + _logger.Warn("Unable to remove item from nzbget's history, Unknown ID: {0}", id); + return; + } + + if (!EditQueue("HistoryDelete", 0, "", item.Id, settings)) + { + _logger.Warn("Failed to remove item from nzbget history, {0} [{1}]", item.Name, item.Id); + } + } + + private bool EditQueue(string command, int offset, string editText, int id, NzbgetSettings settings) + { + var parameters = new object[] { command, offset, editText, id }; + var request = BuildRequest(new JsonRequest("editqueue", parameters)); + var response = Json.Deserialize(ProcessRequest(request, settings)); + + return response.Result; + } + private string ProcessRequest(IRestRequest restRequest, NzbgetSettings settings) { var client = BuildClient(settings); diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index e3b987e4c..51f18cac5 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -3,6 +3,7 @@ using System.IO; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; using NzbDrone.Core.Instrumentation.Extensions; @@ -35,7 +36,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var request = new RestRequest(Method.POST); var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority); - request.AddFile("name", ReadFully(nzb), title, "application/x-nzb"); + request.AddFile("name", nzb.ToBytes(), title, "application/x-nzb"); SabnzbdAddResponse response; @@ -161,20 +162,5 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (result.Failed) throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error); } - - //TODO: Find a better home for this - private byte[] ReadFully(Stream input) - { - byte[] buffer = new byte[16 * 1024]; - using (MemoryStream ms = new MemoryStream()) - { - int read; - while ((read = input.Read(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); - } - return ms.ToArray(); - } - } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index ceaf945a7..b38131161 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using NLog; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 3f75a0767..6010d826c 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -236,6 +236,8 @@ + + @@ -500,10 +502,10 @@ - + - + From 55a808a87b08d364a33f1438e3b6246bdeb3613b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 20 Mar 2014 00:33:18 -0700 Subject: [PATCH 09/35] Fixed broken build --- .../NzbgetTests/DownloadNzbFixture.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs index b0c8d2efb..848bc237e 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs @@ -1,8 +1,10 @@ using System; +using System.IO; using System.Linq; using FizzWare.NBuilder; using Moq; using NUnit.Framework; +using NzbDrone.Common; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Parser.Model; @@ -46,16 +48,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests [Test] public void should_add_item_to_queue() { - var p = new object[] {"30.Rock.S01E01.Pilot.720p.hdtv.nzb", "TV", 50, false, "http://www.nzbdrone.com"}; - Mocker.GetMock() - .Setup(s => s.AddNzb(It.IsAny(), p)) - .Returns(true); + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns("id"); Subject.DownloadNzb(_remoteEpisode); Mocker.GetMock() - .Verify(v => v.AddNzb(It.IsAny(), It.IsAny()), Times.Once()); + .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } } } From 78ef0d6fb03b2aceccbe85b72bf49005bd0204aa Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 20 Mar 2014 00:39:24 -0700 Subject: [PATCH 10/35] Fixed broken in queue test for nzbget --- .../Download/DownloadClientTests/NzbgetTests/QueueFixture.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs index 4fbbbad74..ec7befef0 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs @@ -24,6 +24,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests _queue = Builder.CreateListOfSize(5) .All() .With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb") + .With(q => q.Parameters = new List + { + new NzbgetParameter { Name = "drone", Value = "id" } + }) .Build() .ToList(); From f20c40a24de3397551f54ccf96378708958807c0 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 20 Mar 2014 22:28:43 +0100 Subject: [PATCH 11/35] VS2013 automatically adds these entries for NUnit integration. --- src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj | 3 +++ src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj | 3 +++ src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj | 3 +++ src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj | 3 +++ src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj | 3 +++ .../NzbDrone.Integration.Test.csproj | 3 +++ src/NzbDrone.Libraries.Test/NzbDrone.Libraries.Test.csproj | 4 +++- src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj | 3 +++ src/NzbDrone.Update.Test/NzbDrone.Update.Test.csproj | 3 +++ src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj | 3 +++ 10 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj index 6759bccf3..661e3279d 100644 --- a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj +++ b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj @@ -95,6 +95,9 @@ + + +