diff --git a/.editorconfig b/.editorconfig index dad58944a..5e19cc2d6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,14 +2,14 @@ # editorconfig.org root = true -[*.{cs,html,js,hbs}] +[*.{cs}] charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 4 -[*.less] +[*.{js,html,js,hbs,less,css}] charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ae50843d..68759770b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,21 +7,7 @@ Setup guides, FAQ, the more information we have on the wiki the better. ## Development ## -### Tools required ### -- Visual Studio 2015 -- HTML/Javascript editor of choice (Sublime Text/Webstorm/Atom/etc) -- npm (node package manager) -- git - -### Getting started ### - -1. Fork Radarr -2. Clone (develop branch) *you may need pull in submodules separately if you client doesn't clone them automatically (CurlSharp)* -3. Run `npm install` -4. Run `npm start` - Used to compile the UI components and copy them. - Leave this window open. - If you have gulp globally installed you can use `gulp watch` instead -5. Compile in Visual Studio +See the readme for information on setting up your development environment. ### Contributing Code ### - If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Radarr/Radarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first) diff --git a/README.md b/README.md index 31fcebb37..3014d6816 100644 --- a/README.md +++ b/README.md @@ -107,14 +107,15 @@ See the [Roadmap blogpost](https://blog.radarr.video/development/update/2018/11/ * [Visual Studio Community 2017](https://www.visualstudio.com/vs/community/) or [Rider](http://www.jetbrains.com/rider/) * [Git](https://git-scm.com/downloads) * [Node.js](https://nodejs.org/en/download/) +* [Yarn](https://yarnpkg.com/) ### Setup * Make sure all the required software mentioned above are installed * Clone the repository into your development machine ([*info*](https://help.github.com/desktop/guides/contributing/working-with-your-remote-repository-on-github-or-github-enterprise)) * Grab the submodules `git submodule init && git submodule update` -* Install the required Node Packages `npm install` -* Start gulp to monitor your dev environment for any changes that need post processing using `npm start` command. +* Install the required Node Packages `yarn install` +* Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command. > **Notice** > Gulp must be running at all times while you are working with Radarr client source files. @@ -127,7 +128,7 @@ See the [Roadmap blogpost](https://blog.radarr.video/development/update/2018/11/ ### Development -* Open `NzbDrone.sln` in Visual Studio 2017 or run the build.sh script, if Mono is installed. Alternatively you can use Jetbrains Rider, since it works on all Platforms. +* Open `Radarr.sln` in Visual Studio 2017 or run the build.sh script, if Mono is installed. Alternatively you can use Jetbrains Rider, since it works on all Platforms. * Make sure `NzbDrone.Console` is set as the startup project * Run `build.sh` before running @@ -158,4 +159,4 @@ Thank you to [JetBrains JetBrai ## License * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -* Copyright 2010-2018 +* Copyright 2010-2019 diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js index 5f51a270c..294fcbd9b 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.js +++ b/frontend/src/Activity/History/Details/HistoryDetails.js @@ -163,7 +163,7 @@ function HistoryDetails(props) { ); } - if (eventType === 'episodeFileDeleted') { + if (eventType === 'movieFileDeleted') { const { reason } = data; @@ -199,7 +199,7 @@ function HistoryDetails(props) { ); } - if (eventType === 'episodeFileRenamed') { + if (eventType === 'movieFileRenamed') { const { sourcePath, sourceRelativePath, diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.js b/frontend/src/Activity/History/Details/HistoryDetailsModal.js index 2cf9294f6..51e2fc355 100644 --- a/frontend/src/Activity/History/Details/HistoryDetailsModal.js +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.js @@ -18,11 +18,11 @@ function getHeaderTitle(eventType) { case 'downloadFailed': return 'Download Failed'; case 'downloadFolderImported': - return 'Episode Imported'; - case 'episodeFileDeleted': - return 'Episode File Deleted'; - case 'episodeFileRenamed': - return 'Episode File Renamed'; + return 'Movie Imported'; + case 'movieFileDeleted': + return 'Movie File Deleted'; + case 'movieFileRenamed': + return 'Movie File Renamed'; default: return 'Unknown'; } diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index f3e334ff8..379cc4455 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -71,6 +71,7 @@ class QueueRow extends Component { quality, protocol, indexer, + outputPath, downloadClient, estimatedCompletionTime, timeleft, @@ -195,6 +196,14 @@ class QueueRow extends Component { ); } + if (name === 'outputPath') { + return ( + + {outputPath} + + ); + } + if (name === 'estimatedCompletionTime') { return ( state.queue.options.includeUnknownMovieItems, (app, status, includeUnknownMovieItems) => { const { + errors, + warnings, + unknownErrors, + unknownWarnings, count, - unknownCount + totalCount } = status.item; return { @@ -21,7 +25,9 @@ function createMapStateToProps() { isReconnecting: app.isReconnecting, isPopulated: status.isPopulated, ...status.item, - count: includeUnknownMovieItems ? count : count - unknownCount + count: includeUnknownMovieItems ? totalCount : count, + errors: includeUnknownMovieItems ? errors || unknownErrors : errors, + warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings }; } ); diff --git a/frontend/src/Calendar/Events/CalendarEventConnector.js b/frontend/src/Calendar/Events/CalendarEventConnector.js index 7da98888e..1ee8d0bc5 100644 --- a/frontend/src/Calendar/Events/CalendarEventConnector.js +++ b/frontend/src/Calendar/Events/CalendarEventConnector.js @@ -13,10 +13,10 @@ function createMapStateToProps() { createMovieFileSelector(), createQueueItemSelector(), createUISettingsSelector(), - (calendarOptions, series, episodeFile, queueItem, uiSettings) => { + (calendarOptions, movie, movieFile, queueItem, uiSettings) => { return { - series, - episodeFile, + movie, + movieFile, queueItem, ...calendarOptions, timeFormat: uiSettings.timeFormat, diff --git a/frontend/src/Calendar/Events/CalendarEventGroupConnector.js b/frontend/src/Calendar/Events/CalendarEventGroupConnector.js index e13e5b998..483b25467 100644 --- a/frontend/src/Calendar/Events/CalendarEventGroupConnector.js +++ b/frontend/src/Calendar/Events/CalendarEventGroupConnector.js @@ -6,11 +6,11 @@ import CalendarEventGroup from './CalendarEventGroup'; function createIsDownloadingSelector() { return createSelector( - (state, { episodeIds }) => episodeIds, + (state, { movieIds }) => movieIds, (state) => state.queue.details, - (episodeIds, details) => { + (movieIds, details) => { return details.items.some((item) => { - return episodeIds.includes(item.episode.id); + return item.movie && movieIds.includes(item.movie.id); }); } ); @@ -22,9 +22,9 @@ function createMapStateToProps() { createMovieSelector(), createIsDownloadingSelector(), createUISettingsSelector(), - (calendarOptions, series, isDownloading, uiSettings) => { + (calendarOptions, movie, isDownloading, uiSettings) => { return { - series, + movie, isDownloading, ...calendarOptions, timeFormat: uiSettings.timeFormat, diff --git a/frontend/src/Calendar/Header/CalendarHeader.js b/frontend/src/Calendar/Header/CalendarHeader.js index 4fea8356d..9e65df87b 100644 --- a/frontend/src/Calendar/Header/CalendarHeader.js +++ b/frontend/src/Calendar/Header/CalendarHeader.js @@ -83,6 +83,7 @@ class CalendarHeader extends Component { end, longDateFormat, isSmallScreen, + collapseViewButtons, onTodayPress, onPreviousPress, onNextPress @@ -145,7 +146,7 @@ class CalendarHeader extends Component { } { - isSmallScreen ? + collapseViewButtons ? + { + isSmallScreen ? + null : + + Month + + } + +
Search for {query}
); diff --git a/frontend/src/Movie/Index/MovieIndexFooterConnector.js b/frontend/src/Movie/Index/MovieIndexFooterConnector.js index dee1f9629..13f0cb915 100644 --- a/frontend/src/Movie/Index/MovieIndexFooterConnector.js +++ b/frontend/src/Movie/Index/MovieIndexFooterConnector.js @@ -1,10 +1,40 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import MovieIndexFooter from './MovieIndexFooter'; +function createUnoptimizedSelector() { + return createSelector( + createClientSideCollectionSelector('movies', 'movieIndex'), + (movies) => { + return movies.items.map((s) => { + const { + monitored, + status, + statistics + } = s; + + return { + monitored, + status, + statistics + }; + }); + } + ); +} + +function createMoviesSelector() { + return createDeepEqualSelector( + createUnoptimizedSelector(), + (movies) => movies + ); +} + function createMapStateToProps() { return createSelector( - (state) => state.movies.items, + createMoviesSelector(), (movies) => { return { movies diff --git a/frontend/src/Settings/General/BackupSettings.js b/frontend/src/Settings/General/BackupSettings.js index 8b416fbc1..f0f9316e2 100644 --- a/frontend/src/Settings/General/BackupSettings.js +++ b/frontend/src/Settings/General/BackupSettings.js @@ -32,7 +32,7 @@ function BackupSettings(props) { Folder } -
+
{ - seriesTokens.map(({ token, example }) => { + movieTokens.map(({ token, example }) => { return (
-
+
{ - seriesIdTokens.map(({ token, example }) => { + movieIdTokens.map(({ token, example }) => { return (
- { - season && -
-
- { - seasonTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- } - - { - episode && -
-
-
- { - episodeTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- - { - daily && -
-
- { - airDateTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- } - - { - anime && -
-
- { - absoluteTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- } -
- } - { additional &&
-
-
- { - episodeTitleTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
-
{ @@ -529,20 +370,12 @@ NamingModal.propTypes = { value: PropTypes.string.isRequired, isOpen: PropTypes.bool.isRequired, advancedSettings: PropTypes.bool.isRequired, - season: PropTypes.bool.isRequired, - episode: PropTypes.bool.isRequired, - daily: PropTypes.bool.isRequired, - anime: PropTypes.bool.isRequired, additional: PropTypes.bool.isRequired, onInputChange: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; NamingModal.defaultProps = { - season: false, - episode: false, - daily: false, - anime: false, additional: false }; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index 299c98936..c895cb6bc 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -1,6 +1,6 @@ .option { display: flex; - align-items: center; + align-items: stretch; flex-wrap: wrap; margin: 3px; border: 1px solid $borderColor; @@ -17,7 +17,7 @@ } .small { - width: 420px; + width: 480px; } .large { @@ -32,6 +32,9 @@ } .example { + display: flex; + align-items: center; + align-self: stretch; flex: 0 0 50%; padding: 6px 16px; background-color: #ddd; diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js index 143e5491a..29c2b1465 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notification.js +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -84,7 +84,7 @@ class Notification extends Component { { supportsOnDownload && onDownload && } diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js index f338b3f1f..24c2d3548 100644 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.js +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.js @@ -19,6 +19,11 @@ class MoreInfo extends Component { radarr.video + Discord + + discord.gg/AD3UP37 + + Wiki github.com/Radarr/Radarr/wiki diff --git a/src/Libraries/MediaInfo/MediaInfo.dll b/src/Libraries/MediaInfo/MediaInfo.dll index ca4ce4fb6..e877e7599 100644 Binary files a/src/Libraries/MediaInfo/MediaInfo.dll and b/src/Libraries/MediaInfo/MediaInfo.dll differ diff --git a/src/Libraries/MediaInfo/libmediainfo.0.dylib b/src/Libraries/MediaInfo/libmediainfo.0.dylib index 73ff0ba4f..0ffd2fdcc 100644 Binary files a/src/Libraries/MediaInfo/libmediainfo.0.dylib and b/src/Libraries/MediaInfo/libmediainfo.0.dylib differ diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index b6d3edb00..b072ff395 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Net; using System.Threading; using FluentAssertions; @@ -24,18 +25,64 @@ namespace NzbDrone.Common.Test.Http [TestFixture(typeof(CurlHttpDispatcher))] public class HttpClientFixture : TestBase where TDispatcher : IHttpDispatcher { - private static string[] _httpBinHosts = new[] { "eu.httpbin.org", "httpbin.org" }; - private static int _httpBinRandom; + private string[] _httpBinHosts; + private int _httpBinSleep; + private int _httpBinRandom; private string _httpBinHost; + private string _httpBinHost2; + + [OneTimeSetUp] + public void FixtureSetUp() + { + var candidates = new[] { "eu.httpbin.org", /*"httpbin.org",*/ "www.httpbin.org" }; + // httpbin.org is broken right now, occassionally redirecting to https if it's unavailable. + _httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray(); + + TestLogger.Info($"{candidates.Length} TestSites available."); + + _httpBinSleep = _httpBinHosts.Count() < 2 ? 100 : 10; + } + + private bool IsTestSiteAvailable(string site) + { + try + { + var req = WebRequest.Create($"http://{site}/get") as HttpWebRequest; + var res = req.GetResponse() as HttpWebResponse; + if (res.StatusCode != HttpStatusCode.OK) return false; + + try + { + req = WebRequest.Create($"http://{site}/status/429") as HttpWebRequest; + res = req.GetResponse() as HttpWebResponse; + } + catch (WebException ex) + { + res = ex.Response as HttpWebResponse; + } + + if (res == null || res.StatusCode != (HttpStatusCode)429) return false; + + return true; + } + catch + { + return false; + } + } [SetUp] public void SetUp() { + if (!_httpBinHosts.Any()) + { + Assert.Inconclusive("No TestSites available"); + } Mocker.GetMock().Setup(c => c.Version).Returns(new Version("1.0.0")); Mocker.GetMock().Setup(c => c.Name).Returns("TestOS"); Mocker.GetMock().Setup(c => c.Version).Returns("9.0.0"); - + Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); @@ -51,6 +98,13 @@ namespace NzbDrone.Common.Test.Http // Roundrobin over the two servers, to reduce the chance of hitting the ratelimiter. _httpBinHost = _httpBinHosts[_httpBinRandom++ % _httpBinHosts.Length]; + _httpBinHost2 = _httpBinHosts[_httpBinRandom % _httpBinHosts.Length]; + } + + [TearDown] + public void TearDown() + { + Thread.Sleep(_httpBinSleep); } [Test] @@ -76,11 +130,12 @@ namespace NzbDrone.Common.Test.Http [Test] public void should_execute_typed_get() { - var request = new HttpRequest($"http://{_httpBinHost}/get"); + var request = new HttpRequest($"http://{_httpBinHost}/get?test=1"); var response = Subject.Get(request); - response.Resource.Url.Should().Be(request.Url.FullUri); + response.Resource.Url.EndsWith("/get?test=1"); + response.Resource.Args.Should().Contain("test", "1"); } [Test] @@ -163,6 +218,11 @@ namespace NzbDrone.Common.Test.Http [Test] public void should_follow_redirects_to_https() { + if (typeof(TDispatcher) == typeof(ManagedHttpDispatcher) && PlatformInfo.IsMono) + { + Assert.Ignore("Will fail on tls1.2 via managed dispatcher, ignore."); + } + var request = new HttpRequestBuilder($"http://{_httpBinHost}/redirect-to") .AddQueryParam("url", $"https://sonarr.tv/") .Build(); @@ -241,7 +301,12 @@ namespace NzbDrone.Common.Test.Http public void GivenOldCookie() { - var oldRequest = new HttpRequest("http://eu.httpbin.org/get"); + if (_httpBinHost == _httpBinHost2) + { + Assert.Inconclusive("Need both httpbin.org and eu.httpbin.org to run this test."); + } + + var oldRequest = new HttpRequest($"http://{_httpBinHost2}/get"); oldRequest.Cookies["my"] = "cookie"; var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.GetMock().Object, Mocker.Resolve()); @@ -258,7 +323,7 @@ namespace NzbDrone.Common.Test.Http { GivenOldCookie(); - var request = new HttpRequest("http://eu.httpbin.org/get"); + var request = new HttpRequest($"http://{_httpBinHost2}/get"); var response = Subject.Get(request); @@ -274,7 +339,7 @@ namespace NzbDrone.Common.Test.Http { GivenOldCookie(); - var request = new HttpRequest("http://httpbin.org/get"); + var request = new HttpRequest($"http://{_httpBinHost}/get"); var response = Subject.Get(request); @@ -334,6 +399,28 @@ namespace NzbDrone.Common.Test.Http responseCookies.Resource.Cookies.Should().BeEmpty(); } + [Test] + public void should_clear_request_cookie() + { + var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies"); + requestSet.Cookies.Add("my", "cookie"); + requestSet.AllowAutoRedirect = false; + requestSet.StoreRequestCookie = true; + requestSet.StoreResponseCookie = false; + + var responseSet = Subject.Get(requestSet); + + var requestClear = new HttpRequest($"http://{_httpBinHost}/cookies"); + requestClear.Cookies.Add("my", null); + requestClear.AllowAutoRedirect = false; + requestClear.StoreRequestCookie = true; + requestClear.StoreResponseCookie = false; + + var responseClear = Subject.Get(requestClear); + + responseClear.Resource.Cookies.Should().BeEmpty(); + } + [Test] public void should_not_store_response_cookie() { @@ -518,20 +605,6 @@ namespace NzbDrone.Common.Test.Http ExceptionVerification.IgnoreErrors(); } - [Test] - public void should_not_send_old_cookie() - { - GivenOldCookie(); - - var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies"); - requestCookies.IgnorePersistentCookies = true; - requestCookies.StoreRequestCookie = false; - requestCookies.StoreResponseCookie = false; - var responseCookies = Subject.Get(requestCookies); - - responseCookies.Resource.Cookies.Should().BeEmpty(); - } - [Test] public void should_throw_on_http429_too_many_requests() { @@ -610,8 +683,7 @@ namespace NzbDrone.Common.Test.Http { try { - string url = - $"http://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeUriString(malformedCookie)}"; + string url = $"http://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeUriString(malformedCookie)}"; var requestSet = new HttpRequest(url); requestSet.AllowAutoRedirect = false; @@ -635,6 +707,7 @@ namespace NzbDrone.Common.Test.Http public class HttpBinResource { + public Dictionary Args { get; set; } public Dictionary Headers { get; set; } public string Origin { get; set; } public string Url { get; set; } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index b3d05e7f6..5625e954f 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -21,13 +21,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests { Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = new QBittorrentSettings - { - Host = "127.0.0.1", - Port = 2222, - Username = "admin", - Password = "pass", - MovieCategory = "movies-radarr" - }; + { + Host = "127.0.0.1", + Port = 2222, + Username = "admin", + Password = "pass", + MovieCategory = "movies-radarr" + }; Mocker.GetMock() .Setup(s => s.GetHashFromTorrentFile(It.IsAny())) diff --git a/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs b/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs index 2ac169b30..39d382bbd 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs @@ -4,11 +4,9 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Nyaa; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Test.Common.Categories; namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests @@ -30,40 +28,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests }; } - [Test] - public void nyaa_fetch_recent() - { - var indexer = Mocker.Resolve(); - - indexer.Definition = new IndexerDefinition - { - Name = "MyIndexer", - Settings = new NyaaSettings() - }; - - var result = indexer.FetchRecent(); - - ValidateTorrentResult(result, hasSize: true); - } - - [Test] - public void nyaa_search_single() - { - var indexer = Mocker.Resolve(); - - indexer.Definition = new IndexerDefinition - { - Name = "MyIndexer", - Settings = new NyaaSettings() - }; - - var result = indexer.Fetch(_singleSearchCriteria); - - ValidateTorrentResult(result, hasSize: true); - } - - - private void ValidateTorrentResult(IList reports, bool hasSize = false, bool hasInfoUrl = false, bool hasMagnet = false) { reports.Should().OnlyContain(c => c.GetType() == typeof(TorrentInfo)); diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesCommandServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesCommandServiceFixture.cs deleted file mode 100644 index 3eebc70d4..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesCommandServiceFixture.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Disk; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.TrackedDownloads; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Commands; -using NzbDrone.Core.MediaFiles.EpisodeImport; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.MediaFiles -{ - [TestFixture] - public class DownloadedEpisodesCommandServiceFixture : CoreTest - { - private string _downloadFolder = "c:\\drop_other\\Show.S01E01\\".AsOsAgnostic(); - private string _downloadFile = "c:\\drop_other\\Show.S01E01.mkv".AsOsAgnostic(); - - private TrackedDownload _trackedDownload; - - [SetUp] - public void Setup() - { - Mocker.GetMock() - .Setup(v => v.ProcessRootFolder(It.IsAny())) - .Returns(new List()); - - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new List()); - - var downloadItem = Builder.CreateNew() - .With(v => v.DownloadId = "sab1") - .With(v => v.Status = DownloadItemStatus.Downloading) - .Build(); - - var remoteEpisode = Builder.CreateNew() - .With(v => v.Series = new Series()) - .Build(); - - _trackedDownload = new TrackedDownload - { - DownloadItem = downloadItem, - RemoteEpisode = remoteEpisode, - State = TrackedDownloadStage.Downloading - }; - } - - private void GivenExistingFolder(string path) - { - Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) - .Returns(true); - } - - private void GivenExistingFile(string path) - { - Mocker.GetMock().Setup(c => c.FileExists(It.IsAny())) - .Returns(true); - } - - private void GivenValidQueueItem() - { - Mocker.GetMock() - .Setup(s => s.Find("sab1")) - .Returns(_trackedDownload); - } - - [Test] - public void should_skip_import_if_dronefactory_doesnt_exist() - { - Assert.Throws(() => Subject.Execute(new DownloadedEpisodesScanCommand())); - - Mocker.GetMock().Verify(c => c.ProcessRootFolder(It.IsAny()), Times.Never()); - } - - [Test] - public void should_process_folder_if_downloadclientid_is_not_specified() - { - GivenExistingFolder(_downloadFolder); - - Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder }); - - Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Once()); - } - - [Test] - public void should_process_file_if_downloadclientid_is_not_specified() - { - GivenExistingFile(_downloadFile); - - Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFile }); - - Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Once()); - } - - [Test] - public void should_process_folder_with_downloadclientitem_if_available() - { - GivenExistingFolder(_downloadFolder); - GivenValidQueueItem(); - - Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" }); - - Mocker.GetMock().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once()); - } - - [Test] - public void should_process_folder_without_downloadclientitem_if_not_available() - { - GivenExistingFolder(_downloadFolder); - - Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" }); - - Mocker.GetMock().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, null, null), Times.Once()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_warn_if_neither_folder_or_file_exists() - { - Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder }); - - Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_override_import_mode() - { - GivenExistingFile(_downloadFile); - - Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFile, ImportMode = ImportMode.Copy }); - - Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Copy, null, null), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs deleted file mode 100644 index 0fd99b058..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ /dev/null @@ -1,377 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Disk; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.EpisodeImport; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; -using FluentAssertions; - -namespace NzbDrone.Core.Test.MediaFiles -{ - [TestFixture] - public class DownloadedEpisodesImportServiceFixture : CoreTest - { - private string _droneFactory = "c:\\drop\\".AsOsAgnostic(); - private string[] _subFolders = new[] { "c:\\root\\foldername".AsOsAgnostic() }; - private string[] _videoFiles = new[] { "c:\\root\\foldername\\30.rock.s01e01.ext".AsOsAgnostic() }; - - [SetUp] - public void Setup() - { - Mocker.GetMock().Setup(c => c.GetVideoFiles(It.IsAny(), It.IsAny())) - .Returns(_videoFiles); - - Mocker.GetMock().Setup(c => c.GetDirectories(It.IsAny())) - .Returns(_subFolders); - - Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) - .Returns(true); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(new List()); - } - - private void GivenValidSeries() - { - Mocker.GetMock() - .Setup(s => s.GetSeries(It.IsAny())) - .Returns(Builder.CreateNew().Build()); - } - - [Test] - public void should_search_for_series_using_folder_name() - { - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock().Verify(c => c.GetSeries("foldername"), Times.Once()); - } - - [Test] - public void should_skip_if_file_is_in_use_by_another_process() - { - GivenValidSeries(); - - Mocker.GetMock().Setup(c => c.IsFileLocked(It.IsAny())) - .Returns(true); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - VerifyNoImport(); - } - - [Test] - public void should_skip_if_no_series_found() - { - Mocker.GetMock().Setup(c => c.GetSeries("foldername")).Returns((Series)null); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never()); - - VerifyNoImport(); - } - - [Test] - public void should_not_import_if_folder_is_a_series_path() - { - GivenValidSeries(); - - Mocker.GetMock() - .Setup(s => s.SeriesPathExists(It.IsAny())) - .Returns(true); - - Mocker.GetMock() - .Setup(c => c.GetVideoFiles(It.IsAny(), It.IsAny())) - .Returns(new string[0]); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.GetVideoFiles(It.IsAny(), true), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_not_delete_folder_if_no_files_were_imported() - { - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), false, null, ImportMode.Auto)) - .Returns(new List()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.GetFolderSize(It.IsAny()), Times.Never()); - } - - [Test] - public void should_not_delete_folder_if_files_were_imported_and_video_files_remain() - { - GivenValidSeries(); - - var localEpisode = new LocalEpisode(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null, true)) - .Returns(imported); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(imported.Select(i => new ImportResult(i)).ToList()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_delete_folder_if_files_were_imported_and_only_sample_files_remain() - { - GivenValidSeries(); - - var localEpisode = new LocalEpisode(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null, true)) - .Returns(imported); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(imported.Select(i => new ImportResult(i)).ToList()); - - Mocker.GetMock() - .Setup(s => s.IsSample(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Once()); - } - - [TestCase("_UNPACK_")] - [TestCase("_FAILED_")] - public void should_remove_unpack_from_folder_name(string prefix) - { - var folderName = "30.rock.s01e01.pilot.hdtv-lol"; - var folders = new[] { string.Format(@"C:\Test\Unsorted\{0}{1}", prefix, folderName).AsOsAgnostic() }; - - Mocker.GetMock() - .Setup(c => c.GetDirectories(It.IsAny())) - .Returns(folders); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.GetSeries(folderName), Times.Once()); - - Mocker.GetMock() - .Verify(v => v.GetSeries(It.Is(s => s.StartsWith(prefix))), Times.Never()); - } - - [Test] - public void should_return_importresult_on_unknown_series() - { - Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) - .Returns(false); - - Mocker.GetMock().Setup(c => c.FileExists(It.IsAny())) - .Returns(true); - - var fileName = @"C:\folder\file.mkv".AsOsAgnostic(); - - var result = Subject.ProcessPath(fileName); - - result.Should().HaveCount(1); - result.First().ImportDecision.Should().NotBeNull(); - result.First().ImportDecision.LocalEpisode.Should().NotBeNull(); - result.First().ImportDecision.LocalEpisode.Path.Should().Be(fileName); - result.First().Result.Should().Be(ImportResultType.Rejected); - } - - [Test] - public void should_not_delete_if_there_is_large_rar_file() - { - GivenValidSeries(); - - var localEpisode = new LocalEpisode(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null, true)) - .Returns(imported); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(imported.Select(i => new ImportResult(i)).ToList()); - - Mocker.GetMock() - .Setup(s => s.IsSample(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true); - - Mocker.GetMock() - .Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) - .Returns(new []{ _videoFiles.First().Replace(".ext", ".rar") }); - - Mocker.GetMock() - .Setup(s => s.GetFileSize(It.IsAny())) - .Returns(15.Megabytes()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_use_folder_if_folder_import() - { - GivenValidSeries(); - - var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic(); - var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic(); - - Mocker.GetMock().Setup(c => c.FolderExists(folderName)) - .Returns(true); - - Mocker.GetMock().Setup(c => c.GetFiles(folderName, SearchOption.TopDirectoryOnly)) - .Returns(new[] { fileName }); - - var localEpisode = new LocalEpisode(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - - Subject.ProcessPath(fileName); - - Mocker.GetMock() - .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.Is(v => v.AbsoluteEpisodeNumbers.First() == 9), true), Times.Once()); - } - - [Test] - public void should_not_use_folder_if_file_import() - { - GivenValidSeries(); - - var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\Torrents\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic(); - - Mocker.GetMock().Setup(c => c.FolderExists(fileName)) - .Returns(false); - - Mocker.GetMock().Setup(c => c.FileExists(fileName)) - .Returns(true); - - var localEpisode = new LocalEpisode(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - var result = Subject.ProcessPath(fileName); - - Mocker.GetMock() - .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null, true), Times.Once()); - } - - [Test] - public void should_not_process_if_file_and_folder_do_not_exist() - { - var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic(); - - Mocker.GetMock().Setup(c => c.FolderExists(folderName)) - .Returns(false); - - Mocker.GetMock().Setup(c => c.FileExists(folderName)) - .Returns(false); - - Subject.ProcessPath(folderName).Should().BeEmpty(); - - Mocker.GetMock() - .Verify(v => v.GetSeries(It.IsAny()), Times.Never()); - - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void should_not_delete_if_no_files_were_imported() - { - GivenValidSeries(); - - var localEpisode = new LocalEpisode(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null, true)) - .Returns(imported); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(new List()); - - Mocker.GetMock() - .Setup(s => s.IsSample(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true); - - Mocker.GetMock() - .Setup(s => s.GetFileSize(It.IsAny())) - .Returns(15.Megabytes()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); - } - - private void VerifyNoImport() - { - Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null, ImportMode.Auto), - Times.Never()); - } - - private void VerifyImport() - { - Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null, ImportMode.Auto), - Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs index aef8580d5..5a4372369 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo info.AudioBitrate.Should().Be(128000); info.AudioChannels.Should().Be(2); info.AudioLanguages.Should().Be("English"); - info.AudioAdditionalFeatures.Should().Be("LC"); + info.AudioAdditionalFeatures.Should().BeOneOf("", "LC"); info.Height.Should().Be(320); info.RunTime.Seconds.Should().Be(10); info.ScanType.Should().Be("Progressive"); @@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo info.AudioBitrate.Should().Be(128000); info.AudioChannels.Should().Be(2); info.AudioLanguages.Should().Be("English"); - info.AudioAdditionalFeatures.Should().Be("LC"); + info.AudioAdditionalFeatures.Should().BeOneOf("", "LC"); info.Height.Should().Be(320); info.RunTime.Seconds.Should().Be(10); info.ScanType.Should().Be("Progressive"); diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 0690e4d6e..5e3399eb5 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -27,6 +27,9 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Seed S02E09 HDTV x264-2HD [eztv]-[rarbg.com]", "2HD")] [TestCase("7s-atlantis-s02e01-720p.mkv", null)] [TestCase("The.Middle.720p.HEVC.x265-MeGusta-Pre", "MeGusta")] + [TestCase("Blue.Bloods.S08E05.The.Forgotten.1080p.AMZN.WEB-DL.DDP5.1.H.264-NTb-Rakuv", "NTb")] + [TestCase("Lie.To.Me.S01E13.720p.BluRay.x264-SiNNERS-Rakuvfinhel", "SiNNERS")] + [TestCase("Who.is.America.S01E01.INTERNAL.720p.HDTV.x264-aAF-RakuvUS-Obfuscated", "aAF")] [TestCase("Haunted.Hayride.2018.720p.WEBRip.DDP5.1.x264-NTb-postbot", "NTb")] [TestCase("Haunted.Hayride.2018.720p.WEBRip.DDP5.1.x264-NTb-xpost", "NTb")] //[TestCase("", "")] diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs index e379da197..8956a583d 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs @@ -142,7 +142,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox if (image == null) { _logger.Trace("Failed to find suitable Movie image for movie {0}.", movie.Title); - return null; + return new List(); } var source = _mediaCoverService.GetCoverPath(movie.Id, image.CoverType); diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs index fdd17bf68..c1253c151 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs @@ -110,7 +110,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual if (movie == null) { - movie = trackedDownload.RemoteMovie.Movie; + movie = trackedDownload.RemoteMovie?.Movie; } } diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs new file mode 100644 index 000000000..df1d2eb33 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Notifications.Discord.Payloads; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Discord +{ + public class Discord : NotificationBase + { + private readonly IDiscordProxy _proxy; + + public Discord(IDiscordProxy proxy) + { + _proxy = proxy; + } + + public override string Name => "Discord"; + public override string Link => "https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks"; + + public override void OnGrab(GrabMessage message) + { + var embeds = new List + { + new Embed + { + Description = message.Message, + Title = message.Movie.Title, + Text = message.Message, + Color = (int)DiscordColors.Warning + } + }; + var payload = CreatePayload($"Grabbed: {message.Message}", embeds); + + _proxy.SendPayload(payload, Settings); + } + + public override void OnDownload(DownloadMessage message) + { + var embeds = new List + { + new Embed + { + Description = message.Message, + Title = message.Movie.Title, + Text = message.Message, + Color = (int)DiscordColors.Success + } + }; + var payload = CreatePayload($"Imported: {message.Message}", embeds); + + _proxy.SendPayload(payload, Settings); + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(TestMessage()); + + return new ValidationResult(failures); + } + + public ValidationFailure TestMessage() + { + try + { + var message = $"Test message from Radarr posted at {DateTime.Now}"; + var payload = CreatePayload(message); + + _proxy.SendPayload(payload, Settings); + + } + catch (DiscordException ex) + { + return new NzbDroneValidationFailure("Unable to post", ex.Message); + } + + return null; + } + + private DiscordPayload CreatePayload(string message, List embeds = null) + { + var avatar = Settings.Avatar; + + var payload = new DiscordPayload + { + Username = Settings.Username, + Content = message, + Embeds = embeds + }; + + if (avatar.IsNotNullOrWhiteSpace()) + { + payload.AvatarUrl = avatar; + } + + if (Settings.Username.IsNotNullOrWhiteSpace()) + { + payload.Username = Settings.Username; + } + + return payload; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs new file mode 100644 index 000000000..16590aade --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Notifications.Discord +{ + public enum DiscordColors + { + Danger = 15749200, + Success = 2605644, + Warning = 16753920 + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordException.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordException.cs new file mode 100644 index 000000000..1bc0d6294 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordException.cs @@ -0,0 +1,16 @@ +using System; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Notifications.Discord +{ + class DiscordException : NzbDroneException + { + public DiscordException(string message) : base(message) + { + } + + public DiscordException(string message, Exception innerException, params object[] args) : base(message, innerException, args) + { + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs new file mode 100644 index 000000000..425364253 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs @@ -0,0 +1,46 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Notifications.Discord.Payloads; +using NzbDrone.Core.Rest; + +namespace NzbDrone.Core.Notifications.Discord +{ + public interface IDiscordProxy + { + void SendPayload(DiscordPayload payload, DiscordSettings settings); + } + + public class DiscordProxy : IDiscordProxy + { + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public DiscordProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void SendPayload(DiscordPayload payload, DiscordSettings settings) + { + try + { + var request = new HttpRequestBuilder(settings.WebHookUrl) + .Accept(HttpAccept.Json) + .Build(); + + request.Method = HttpMethod.POST; + request.Headers.ContentType = "application/json"; + request.SetContent(payload.ToJson()); + + _httpClient.Execute(request); + } + catch (RestException ex) + { + _logger.Error(ex, "Unable to post payload {0}", payload); + throw new DiscordException("Unable to post payload", ex); + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs new file mode 100644 index 000000000..e4ba51571 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs @@ -0,0 +1,35 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Discord +{ + public class DiscordSettingsValidator : AbstractValidator + { + public DiscordSettingsValidator() + { + RuleFor(c => c.WebHookUrl).IsValidUrl(); + } + } + + public class DiscordSettings : IProviderConfig + { + private static readonly DiscordSettingsValidator Validator = new DiscordSettingsValidator(); + + [FieldDefinition(0, Label = "Webhook URL", HelpText = "Discord channel webhook url")] + public string WebHookUrl { get; set; } + + [FieldDefinition(1, Label = "Username", HelpText = "The username to post as, defaults to Discord webhook default")] + public string Username { get; set; } + + [FieldDefinition(2, Label = "Avatar", HelpText = "Change the avatar that is used for messages from this integration", Type = FieldType.Textbox)] + public string Avatar { get; set; } + + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordPayload.cs b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordPayload.cs new file mode 100644 index 000000000..37f1f1c3d --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordPayload.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Discord.Payloads +{ + public class DiscordPayload + { + public string Content { get; set; } + + public string Username { get; set; } + + [JsonProperty("avatar_url")] + public string AvatarUrl { get; set; } + + public List Embeds { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs b/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs new file mode 100644 index 000000000..50e27914b --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.Notifications.Discord.Payloads +{ + public class Embed + { + public string Description { get; set; } + public string Title { get; set; } + public string Text { get; set; } + public int Color { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Gotify/GotifyPriority.cs b/src/NzbDrone.Core/Notifications/Gotify/GotifyPriority.cs index 9a5f9c5dc..8782d4dcd 100644 --- a/src/NzbDrone.Core/Notifications/Gotify/GotifyPriority.cs +++ b/src/NzbDrone.Core/Notifications/Gotify/GotifyPriority.cs @@ -1,4 +1,5 @@ namespace NzbDrone.Core.Notifications.Gotify + { public enum GotifyPriority { diff --git a/src/NzbDrone.Core/Notifications/Gotify/GotifyService.cs b/src/NzbDrone.Core/Notifications/Gotify/GotifyProxy.cs similarity index 100% rename from src/NzbDrone.Core/Notifications/Gotify/GotifyService.cs rename to src/NzbDrone.Core/Notifications/Gotify/GotifyProxy.cs diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index 079208f60..e939fddca 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using FluentValidation.Results; -using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Notifications.Slack.Payloads; using NzbDrone.Core.Movies; @@ -13,12 +12,10 @@ namespace NzbDrone.Core.Notifications.Slack public class Slack : NotificationBase { private readonly ISlackProxy _proxy; - private readonly Logger _logger; - public Slack(ISlackProxy proxy, Logger logger) + public Slack(ISlackProxy proxy) { _proxy = proxy; - _logger = logger; } public override string Name => "Slack"; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index da78120f4..5632bde4f 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -964,10 +964,17 @@ + + + + + + + + - diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index e31c57371..99a8a10ac 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -35,18 +35,9 @@ namespace NzbDrone.Core.Organizer private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex TagsRegex = new Regex(@"(?\{tags(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?s?{season(?:\:0+)?}(?[- ._]?[ex])(?{episode(?:\:0+)?}))(?[- ._]+?(?={))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -178,48 +169,6 @@ namespace NzbDrone.Core.Organizer { return new BasicNamingConfig(); //For now let's be lazy - //var episodeFormat = GetEpisodeFormat(nameSpec.StandardMovieFormat).LastOrDefault(); - - //if (episodeFormat == null) - //{ - // return new BasicNamingConfig(); - //} - - //var basicNamingConfig = new BasicNamingConfig - //{ - // Separator = episodeFormat.Separator, - // NumberStyle = episodeFormat.SeasonEpisodePattern - //}; - - //var titleTokens = TitleRegex.Matches(nameSpec.StandardMovieFormat); - - //foreach (Match match in titleTokens) - //{ - // var separator = match.Groups["separator"].Value; - // var token = match.Groups["token"].Value; - - // if (!separator.Equals(" ")) - // { - // basicNamingConfig.ReplaceSpaces = true; - // } - - // if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) - // { - // basicNamingConfig.IncludeSeriesTitle = true; - // } - - // if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) - // { - // basicNamingConfig.IncludeEpisodeTitle = true; - // } - - // if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) - // { - // basicNamingConfig.IncludeQuality = true; - // } - //} - - //return basicNamingConfig; } public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) @@ -521,18 +470,6 @@ namespace NzbDrone.Core.Organizer return value.ToString(split[1]); } - private EpisodeFormat[] GetEpisodeFormat(string pattern) - { - return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() - .Select(match => new EpisodeFormat - { - EpisodeSeparator = match.Groups["episodeSeparator"].Value, - Separator = match.Groups["separator"].Value, - EpisodePattern = match.Groups["episode"].Value, - SeasonEpisodePattern = match.Groups["seasonEpisode"].Value, - }).ToArray()); - } - private string GetQualityProper(Movie movie, QualityModel quality) { if (quality.Revision.Version > 1) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index f38bef2ea..75e099b4b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -113,7 +113,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?(?[1-9]\d{1})(?[0-1][0-9])(?[0-3][0-9]))(?=[_.-])", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex CleanReleaseGroupRegex = new Regex(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|sample|Pre|postbot|xpost))+$", + private static readonly Regex CleanReleaseGroupRegex = new Regex(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|sample|Pre|postbot|xpost|Rakuv[a-z]*|WhiteRev|BUYMORE))+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CleanTorrentSuffixRegex = new Regex(@"\[(?:ettv|rartv|rarbg|cttv)\]$", diff --git a/src/NzbDrone.Integration.Test/HttpLogFixture.cs b/src/NzbDrone.Integration.Test/HttpLogFixture.cs index a314f458c..be474ad0a 100644 --- a/src/NzbDrone.Integration.Test/HttpLogFixture.cs +++ b/src/NzbDrone.Integration.Test/HttpLogFixture.cs @@ -16,10 +16,12 @@ namespace NzbDrone.Integration.Test config.LogLevel = "Trace"; HostConfig.Put(config); + var resultGet = Movies.All(); + var logFile = Path.Combine(_runner.AppData, "logs", "radarr.trace.txt"); var logLines = File.ReadAllLines(logFile); - var result = Movies.InvalidPost(new MovieResource()); + var resultPost = Movies.InvalidPost(new MovieResource()); logLines = File.ReadAllLines(logFile).Skip(logLines.Length).ToArray(); diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index d96cd5935..55014f5b5 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -125,6 +125,9 @@ namespace NzbDrone.Integration.Test public void IntegrationSetUp() { TempDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, "_test_" + DateTime.UtcNow.Ticks); + + // Wait for things to get quiet, otherwise the previous test might influence the current one. + Commands.WaitAll(); } [TearDown] diff --git a/src/NzbDrone.Mono.Test/EnvironmentInfo/ReleaseFileVersionAdapterFixture.cs b/src/NzbDrone.Mono.Test/EnvironmentInfo/ReleaseFileVersionAdapterFixture.cs index 383db510b..f13c58bba 100644 --- a/src/NzbDrone.Mono.Test/EnvironmentInfo/ReleaseFileVersionAdapterFixture.cs +++ b/src/NzbDrone.Mono.Test/EnvironmentInfo/ReleaseFileVersionAdapterFixture.cs @@ -26,4 +26,4 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo info.Version.Should().NotBeNullOrWhiteSpace(); } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs index 56fe88bc0..edbe1ec15 100644 --- a/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs +++ b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs @@ -74,4 +74,4 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters .Verify(c => c.GetFiles(It.IsAny(), SearchOption.TopDirectoryOnly), Times.Never()); } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapterFixture.cs b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapterFixture.cs index 798b99d8f..4f1bee23f 100644 --- a/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapterFixture.cs +++ b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapterFixture.cs @@ -79,4 +79,4 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Mono/Disk/FindDriveType.cs b/src/NzbDrone.Mono/Disk/FindDriveType.cs index 4a14feaf0..efb07c5df 100644 --- a/src/NzbDrone.Mono/Disk/FindDriveType.cs +++ b/src/NzbDrone.Mono/Disk/FindDriveType.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Mono.Disk { { "afpfs", DriveType.Network }, { "apfs", DriveType.Fixed }, + { "fuse.mergerfs", DriveType.Fixed }, { "zfs", DriveType.Fixed } }; diff --git a/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj b/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj index 4843e65e7..725deeac3 100644 --- a/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj +++ b/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj @@ -88,7 +88,7 @@ - + diff --git a/src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs b/src/NzbDrone.SignalR/RadarrPerformanceCounterManager.cs similarity index 98% rename from src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs rename to src/NzbDrone.SignalR/RadarrPerformanceCounterManager.cs index ca5fcf386..901aacbb1 100644 --- a/src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs +++ b/src/NzbDrone.SignalR/RadarrPerformanceCounterManager.cs @@ -3,7 +3,7 @@ using Microsoft.AspNet.SignalR.Infrastructure; namespace NzbDrone.SignalR { - public class SonarrPerformanceCounterManager : IPerformanceCounterManager + public class RadarrPerformanceCounterManager : IPerformanceCounterManager { private readonly IPerformanceCounter _counter = new NoOpPerformanceCounter(); diff --git a/src/NzbDrone.SignalR/SignalrDependencyResolver.cs b/src/NzbDrone.SignalR/SignalrDependencyResolver.cs index f9a4eec07..9007e1d41 100644 --- a/src/NzbDrone.SignalR/SignalrDependencyResolver.cs +++ b/src/NzbDrone.SignalR/SignalrDependencyResolver.cs @@ -17,7 +17,7 @@ namespace NzbDrone.SignalR private SignalRDependencyResolver(IContainer container) { _container = container; - var performanceCounterManager = new SonarrPerformanceCounterManager(); + var performanceCounterManager = new RadarrPerformanceCounterManager(); Register(typeof(IPerformanceCounterManager), () => performanceCounterManager); } diff --git a/src/Radarr.Api.V2/Calendar/CalendarFeedModule.cs b/src/Radarr.Api.V2/Calendar/CalendarFeedModule.cs index 8f86b92e9..6c8e0ded3 100644 --- a/src/Radarr.Api.V2/Calendar/CalendarFeedModule.cs +++ b/src/Radarr.Api.V2/Calendar/CalendarFeedModule.cs @@ -36,7 +36,6 @@ namespace Radarr.Api.V2.Calendar var start = DateTime.Today.AddDays(-pastDays); var end = DateTime.Today.AddDays(futureDays); var unmonitored = false; - //var premiersOnly = false; var tags = new List(); // TODO: Remove start/end parameters in v3, they don't work well for iCal @@ -45,7 +44,6 @@ namespace Radarr.Api.V2.Calendar var queryPastDays = Request.Query.PastDays; var queryFutureDays = Request.Query.FutureDays; var queryUnmonitored = Request.Query.Unmonitored; - // var queryPremiersOnly = Request.Query.PremiersOnly; var queryTags = Request.Query.Tags; if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); @@ -68,11 +66,6 @@ namespace Radarr.Api.V2.Calendar unmonitored = bool.Parse(queryUnmonitored.Value); } - //if (queryPremiersOnly.HasValue) - //{ - // premiersOnly = bool.Parse(queryPremiersOnly.Value); - //} - if (queryTags.HasValue) { var tagInput = (string)queryTags.Value.ToString(); @@ -116,7 +109,7 @@ namespace Radarr.Api.V2.Calendar } var occurrence = calendar.Create(); - occurrence.Uid = "NzbDrone_movie_" + movie.Id + (cinemasRelease ? "_cinemas" : "_physical"); + occurrence.Uid = "Radarr_movie_" + movie.Id + (cinemasRelease ? "_cinemas" : "_physical"); occurrence.Status = movie.Status == MovieStatusType.Announced ? EventStatus.Tentative : EventStatus.Confirmed; occurrence.Start = new CalDateTime(date.Value); diff --git a/src/Radarr.Api.V2/Indexers/ReleaseModule.cs b/src/Radarr.Api.V2/Indexers/ReleaseModule.cs index ea467ddec..6d85599b3 100644 --- a/src/Radarr.Api.V2/Indexers/ReleaseModule.cs +++ b/src/Radarr.Api.V2/Indexers/ReleaseModule.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using FluentValidation; using Nancy; -using Nancy.ModelBinding; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Exceptions; @@ -43,12 +43,12 @@ namespace Radarr.Api.V2.Indexers _downloadService = downloadService; _logger = logger; - GetResourceAll = GetReleases; - Post["/"] = x => DownloadRelease(ReadResourceFromRequest()); - PostValidator.RuleFor(s => s.IndexerId).ValidId(); PostValidator.RuleFor(s => s.Guid).NotEmpty(); + GetResourceAll = GetReleases; + Post["/"] = x => DownloadRelease(ReadResourceFromRequest()); + _remoteMovieCache = cacheManager.GetCache(GetType(), "remoteMovies"); } @@ -69,7 +69,7 @@ namespace Radarr.Api.V2.Indexers } catch (ReleaseDownloadException ex) { - _logger.ErrorException(ex.Message, ex); + _logger.Error(ex, ex.Message); throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); } diff --git a/src/Radarr.Api.V2/Indexers/ReleasePushModule.cs b/src/Radarr.Api.V2/Indexers/ReleasePushModule.cs index 8ff2edf0e..dddb0eaf2 100644 --- a/src/Radarr.Api.V2/Indexers/ReleasePushModule.cs +++ b/src/Radarr.Api.V2/Indexers/ReleasePushModule.cs @@ -56,10 +56,10 @@ namespace Radarr.Api.V2.Indexers if (firstDecision?.RemoteMovie.ParsedMovieInfo == null) { - throw new ValidationException(new List { new ValidationFailure("Title", "Unable to parse", release.Title) }); + throw new ValidationException(new List{ new ValidationFailure("Title", "Unable to parse", release.Title) }); } - return MapDecisions(new[] { firstDecision }).AsResponse(); + return MapDecisions(new [] { firstDecision }).AsResponse(); } private void ResolveIndexer(ReleaseInfo release) @@ -74,7 +74,7 @@ namespace Radarr.Api.V2.Indexers } else { - _logger.Debug("Push Release {0} not associated with unknown indexer {1}.", release.Title, release.Indexer); + _logger.Debug("Push Release {0} not associated with known indexer {1}.", release.Title, release.Indexer); } } else if (release.IndexerId != 0 && release.Indexer.IsNullOrWhiteSpace()) @@ -87,7 +87,7 @@ namespace Radarr.Api.V2.Indexers } catch (ModelNotFoundException) { - _logger.Debug("Push Release {0} not associated with unknown indexer {0}.", release.Title, release.IndexerId); + _logger.Debug("Push Release {0} not associated with known indexer {0}.", release.Title, release.IndexerId); release.IndexerId = 0; } } diff --git a/src/Radarr.Api.V2/Indexers/ReleaseResource.cs b/src/Radarr.Api.V2/Indexers/ReleaseResource.cs index 14070abe4..19700c768 100644 --- a/src/Radarr.Api.V2/Indexers/ReleaseResource.cs +++ b/src/Radarr.Api.V2/Indexers/ReleaseResource.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; @@ -46,11 +47,16 @@ namespace Radarr.Api.V2.Indexers public int? Leechers { get; set; } public DownloadProtocol Protocol { get; set; } - public bool IsDaily { get; set; } public bool IsAbsoluteNumbering { get; set; } public bool IsPossibleSpecialEpisode { get; set; } public bool Special { get; set; } + + // Sent when queuing an unknown release + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? MovieId { get; set; } + } public static class ReleaseResourceMapper @@ -88,7 +94,7 @@ namespace Radarr.Api.V2.Indexers CommentUrl = releaseInfo.CommentUrl, DownloadUrl = releaseInfo.DownloadUrl, InfoUrl = releaseInfo.InfoUrl, - // DownloadAllowed = remoteMovie.DownloadAllowed, + DownloadAllowed = remoteMovie.DownloadAllowed, //ReleaseWeight